react-performance について
Reactコードの作成支援と変更レビューを行い、パフォーマンスと設計パターンを確認するスキルです。プロジェクトのReactバージョンやライブラリを調べ、該当する指摘を場所と具体的な修正案付きで示します。
主な機能
- 01アドバイザーとレビュアーのモード
- 02不要な再レンダーと無効なメモ化の確認
- 03Context・ref・古いクロージャの検査
- 04非同期フェッチと競合状態のレビュー
- 05Reactのバージョンと利用ライブラリを考慮
活用シーン
- 01Reactコードを書く前にコンポーネント構成を検討
- 02変更に含まれる不要な再レンダーをレビュー
- 03古い依存関係やクロージャの問題を特定
- 04フェッチのウォーターフォールや競合を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ robsonrung/ rar-skills --skill react-performanceインストール後、Claude Code で/react-performanceを入力して読み込まれたか確認します。
同梱ファイルreferences/cheatsheet.md
詳細
React のパフォーマンスとパターンに関する視点
Nadia Makarevich の Advanced React のルールに基づいて、React コードに助言し、レビューします。 確認するのは、必要以上に再レンダーされるか、メモ化が実際に効果を発揮しているか、このクロージャーが古い値を参照していないか、非同期処理の経路が安全かです。ロジックエラーを探すためのバグ調査ではありません。その場合はコードレビューを行ってください。
状況に応じて、次の 2 つのモードを選びます。
- アドバイザー(新しい React コードの作成中):コードを書く 前に 判断の指針を示します。
memoよりコンポジションを優先し、状態を適切な場所に置き、フェッチを構成します。 - レビュアー(既存コードまたは変更されたコード):以下のチェック項目に照らして監査し、視点ごとに指摘をまとめます。
file:lineを示し、ルールに名前を付け、具体的な修正案を提示します。該当しない視点は無理に含めず、省略してください。コンポーネントに問題がなければ、その旨を明確に伝えてください。
まず対象を特定する(適用前に確認)
以下の各視点はすべて React 17 以降を対象とします。バージョンによって異なるのは、いくつかの具体的なルールです。そのため、推測せずに確認してください。
- React のバージョン。 対象リポジトリの
package.json(react/react-dom)を確認し、バージョン範囲が広い場合はロックファイルも確認します。メジャーバージョンを記録してください。 - 状態管理ライブラリ。 Redux Toolkit、Zustand、Jotai、MobX、TanStack Query、SWR、Apollo などの外部ストアがあるか、またサーバー状態をすでに管理しているかを確認します。
- UI ライブラリ。 MUI、Chakra、Radix、Headless UI、Ant、shadcn/ui、Tailwind のみを使った構成、または独自実装のコンポーネントがあるかを確認します。
確認した内容に基づいて各視点を適用し、バージョン固有のルールは必ず明示してください。特定バージョンの動作を決めつけないようにします。
- Refs と
forwardRef。 React 19 より前では、関数コンポーネントがrefを受け取るにはforwardRefが必要です。React 19 ではrefは通常の props であり、forwardRefは不要です。どちらの書き方が誤りかを指摘する前に、確認したバージョンを確認してください。 - 自動バッチ処理。 React 17 では React のイベントハンドラー内でのみバッチ処理されます。React 18 以降では、あらゆる場所でバッチ処理されます。「なぜ 2 回レンダーされたのか」という指摘は、使用しているバージョンによって変わります。
- 指摘がどのバージョンを前提にしているかを明記します。「確認した React 17 では……」のように書くと、ルールだけを示すより明確です。
特に繰り返し登場する、確認結果に基づく判断が 2 つあります。
- 深く、再レンダーの影響を受けやすいコンポーネントツリー(よくある例は MUI など、スタイル付きコンポーネントを多用するライブラリ)では、不要な親コンポーネントの再レンダーが大きな負荷になります。スタイル付きラッパーすべてに連鎖するためです。このようなケースでは、レンズ 1 をより重視してください。
- メモ化されたセレクターを持つ外部ストアがすでにある場合は、ツリーをまたぐ共有状態に手書きの Context を使うより、そのストアのセレクターを利用してください。Context は更新頻度の低い局所的な設定(テーマ、現在のエンティティなど)に使い、頻繁に変化する状態には使わないでください。そのようなストアがない場合は、レンズ 3 の Context に関する指針がそのまま答えになります。
この書籍(2023 年)は React 17/18 によく合致しています。確認したバージョンがそれより新しい場合は、書籍の前提ではなく、検出したフレームワークの文書化された動作を優先してください。
黄金律(以下のすべての視点に適用)
まずコンポジション、メモ化は最後です。
React.memoは、コンポジションの手法で解決できなかった場合に使う 最後の手段です。すべての props を正しくメモ化するのは見た目以上に難しく、プリミティブ値以外の props を 1 つでも見落とすと、気付かないうちに効果がなくなるためです。
レンズ 1 — 不要な再レンダリング(ch 1–4)
状態が更新されると、所有元のコンポーネントとすべての子コンポーネントが、props に関係なく再レンダリングされます。props が関係するのは、React.memo を使う場合だけです。つまり、最も手軽な解決策はメモ化ではなく、構造を見直すことです。
次の順に確認してください。
- 状態を下に移動する。 入力値、ホバー状態、開閉トグルなど、頻繁に変わる状態をツリーの高い位置で保持しているために、大きなサブツリーが再レンダリングされていませんか? 状態と、それを使う小さな部分を子コンポーネントに切り出してください。これは、本書で最も効果の高い修正です。
- children/要素を props として渡す。 コンポーネントが状態を持っている一方で、その状態に依存しない重いサブツリーをレンダリングしている場合は、そのサブツリーを
children(または要素 prop)として渡します。props として渡された要素は、親自身の状態が変わっても再レンダリングされません。親の親によって作成されているためです。具体的なパターンについては、チートシートの ch 2 を参照してください。 - 子が親の状態や DOM データを本当に必要とする場合(例:DOM 要素に紐づくロジック)に限り、レンダープロップを使います。「状態を持つロジックの共有」という古いユースケースの約 99% はフックに置き換えられています。ロジックを共有するだけのためにレンダープロップを使わないでください。
危険信号:ページの最上位コンポーネントが入力値を保持している、アプリの半分を包むプロバイダーやレイアウトで useState を使っている、「まず memo で囲もう」と考えている。
レンズ 2 — 何の効果もないメモ化(ch 5)
React は、memo の props とフックの依存配列で、オブジェクト/配列/関数を参照で比較します。useMemo は結果をメモ化し、useCallback は関数自体をメモ化します。
効果のないメモ化に注意してください。useMemo/useCallback が正当化されるのは、次のいずれかに当てはまる場合だけです。
- その値が別のフック(
useEffect/useMemo/useCallback)の依存値である、または - その値が
React.memoでラップされたコンポーネントに渡す prop である、または - その値が渡された先のコンポーネントで、上記のいずれかに該当する。
また、気づかないうちに効果を失っている React.memo にも注意してください。
- メモ化されたコンポーネントに渡す非プリミティブな prop(オブジェクト/配列/関数/
children)が、レンダリングのたびに再作成されていると、メモ化は無意味です。すべての props がプリミティブ値か、参照が安定した値でなければなりません。childrenも prop なので、メモ化を忘れがちです。 - その値が、別のメモ化されていない prop やフックの結果から得られていると、メモ化の連鎖が途切れています。
目安:prop をメモ化するなら、その値の利用先が React.memo でラップされているか、依存値として使われていることを確認してください。そうでなければ、メモ化を削除しましょう。
メモ化されたリスト項目や、再レンダリングのたびに状態が不可解にリセット/保持される問題については、キーと再調整を扱うチートシートの ch 6 を参照してください。
レンズ 3 — Context のパフォーマンス(ch 8)
Context のすべてのコンシューマーは、プロバイダーの value が変わると再レンダリングされます。標準的なメモ化ではこれを防げません。
value={{...}}/value={[a, b]}のように、プロバイダーのvalueがメモ化されていないインラインのオブジェクト/配列になっていないか確認してください。親がレンダリングされるたびに、すべてのコンシューマーが再レンダリングされます。useMemoで囲み、コールバックにはuseCallbackを使ってください。- 互いに無関係な値が複数ある場合は、プロバイダーを分割して、一方の変更で他方のコンシューマーが再レンダリングされないようにします。
useStateをuseReducerに置き換えると、データと API をそれぞれ安定した別の Context に保ちやすくなります。 - Context に実際のセレクターはありません。
React.memoと HOC を組み合わせてセレクターを擬似的に実現することはできます(チートシートの ch 7 を参照)が、そうする必要が出てきたら、前述の検出方法に従って、代わりにリポジトリにすでにある外部ストアを使ってください。実際のセレクター API は、擬似的なものより優れています。
Lens 4 — Refs、クロージャー、古い状態(第 9〜11 章)
- ref は、レンダーをまたいで保持されるミュータブルなボックスです。
ref.currentを変更しても再レンダーは発生せず、同期的に処理されます。保持する必要はあるものの、レンダーを発生させたくない値(タイマー、最新のコールバック、前回の値、DOM ノード)に使用します。UI に表示する値には使用しないでください。 forwardRefは、関数コンポーネントにrefプロパティを渡すために、React 19 より前では必要です。React 19 以降では、refは通常のプロパティです。問題を指摘する前に、検出されたバージョンを確認してください。どちらの場合も、DOM ノードをそのまま公開するのではなく、useImperativeHandleで制御された命令的 API を公開してください。- 古いクロージャー は、ここで最もよくあるバグです。レンダー中に作成された関数は、クロージャーが参照する state と props を「凍結」します。依存配列に不足がある状態でメモ化(
useCallback/useMemo)されていたり、一度 ref に格納されたりすると、古い値を読み続けます。- 検出方法:state/props を参照しているのに依存配列に含まれていない
useCallback/useEffect/useMemo、または更新されないref.currentに格納されたコールバック。 - 対処方法:毎回のレンダーで
useEffect内からref.currentの関数を更新し、その後ref.current()を呼び出します。これにより、常に最新のデータを参照できます。これは、React.memoの子コンポーネントを安定させながら、最新のロジックを呼び出す方法でもあります。
- 検出方法:state/props を参照しているのに依存配列に含まれていない
Lens 5 — デバウンス/スロットル(第 11 章)
debounce/throttle が機能するには、コンポーネントの存続期間を通して同じインスタンスが維持される必要があります。
- レンダー内でインラインに作成された、またはメモ化されていないハンドラー内で作成された
debounce(...)/throttle(...)/setTimeoutの ID を指摘してください。タイマーはレンダーごとに再作成され、正しく実行されません。 - 修正方法:デバウンス関数を一度だけメモ化し(
useMemo/ref)、Lens 4 の ref 更新パターンを使って最新の state にアクセスします(単純にメモ化すると、作成時の state が凍結されます)。
Lens 6 — UI のちらつき/useLayoutEffect(第 12 章)
useEffect 内で DOM のサイズや位置を測定して変更すると、ブラウザーは先に「変更前」のフレームを描画するため、目に見えるちらつきが発生します。useLayoutEffect を使用してください。これは描画前に同期的に実行されます。SSR に関する注意点と正確なパターンについては、チートシートの第 12 章を参照してください。
Lens 7 — ポータルとスタッキングコンテキスト(第 13 章)
モーダル/ツールチップ/ドロップダウンが、祖先要素の overflow によって切り取られたり、スタッキングコンテキストに閉じ込められたりする場合(position: fixed であっても、そこから抜け出すことはできません)は、Portal 経由でレンダーしてください。ほとんどのコンポーネントライブラリのオーバーレイ用プリミティブは、すでにポータルを使用しています(MUI の Modal/Popper、Radix、Headless UI)。検出されたライブラリを確認したうえで、独自に実装され、ポータルを使用していないオーバーレイを指摘してください。CSS のルールとイベントのバブリング動作については、チートシートの第 13 章を参照してください。
Lens 8 — データ取得:ウォーターフォールと競合状態(第 14〜15 章)
- ウォーターフォール: 並列に実行できる逐次的/条件付きの
await。依存関係のあるuseEffectでのデータ取得や、aとbが独立しているにもかかわらずawait a; await bとしている箇所を指摘してください。Promise.all、並列プロミス、またはデータプロバイダー Context を使用します。ブラウザーの並列リクエスト数の上限に注意してください。重要なリソースは、React のマウント前にプリフェッチできます。 - 競合状態: 変化する値(例:
url、ID)を依存キーとするエフェクト内で、await/.thenの後にsetStateすること。先行する遅いリクエストが新しいリクエストの後に完了し、新しいデータを上書きする可能性があります。修正方法(後の 2 つを優先):setStateの前に解決された ID を比較する、useEffectでクリーンアップフラグを使う、AbortControllerを使用する。正確なパターンについては、チートシートの第 15 章を参照してください。(サーバー状態用データレイヤー(RTK Query、TanStack Query、SWR、Apollo)があれば、この問題を処理してくれます。リポジトリに導入されている場合は、サーバー状態の管理に、エフェクト内で独自実装したfetchを使うよりも優先してください。)
Lens 9 — エラー処理(第 16 章)
React 16 以降では、捕捉されないレンダーエラーがアプリ全体のアンマウントを引き起こすため、適切な箇所にいくつかの ErrorBoundary を設けることは必須です。境界ではコールバック/setTimeout/プロミスのエラーを捕捉できません。一方、try/catch ではレンダー/useEffect/ネストされたコンポーネントのエラーを捕捉できません。非同期エラーをレンダーのライフサイクルに再スローして橋渡しするか、react-error-boundary ライブラリを使用してください。正確なフックと対応範囲の一覧については、チートシートの第 16 章を参照してください。
適用方法
- 変更がどのレンズに関わるかを特定します(fetch フック → 8、プロバイダー → 3、メモ化されたコンポーネント → 1、2、4)。
- advisor モードでは、まず構造的な選択肢(状態を下位に移す/コンポジション)を提案し、レンズ 2 のチェック項目を満たした場合にのみ、メモ化を次の手段として検討します。
- reviewer モードでは、レンズごとに次の形式で報告します:
file:line→ 該当するルール → 具体的な修正方法。 - 影響のない再レンダリングを作り出さないでください。軽量で再レンダリングの頻度が低いコンポーネントには、最適化は不要です。状態の更新頻度が高く、サブツリーが重い箇所を最適化してください。
章ごとの判断表とコードスニペットについては、references/cheatsheet.md を参照してください。