Skip to main content

Reactパフォーマンス最適化について学んでいること

5分で読む

はじめに

Reactを何年も使っていましたが、最近、ある不快な事実に気づきました。

なぜあるコンポーネントが再レンダリングされて、別のものがされないのか、明確に説明できなかったのです。

useMemouseCallbackReact.memoは知っていました。使ったことだってあります。 でもその理解は浅かった——パターンに従うだけで、いつなぜ使うのかを本当に理解していなかった。

だから一歩立ち止まり、Reactパフォーマンス最適化を基礎から真剣に学ぶことにしました。

この記事は今学んでいることのまとめです——専門家としてではなく、Reactが実際にどう動くのかのメンタルモデルを作り直している一人として。


今の私が考える「Reactパフォーマンス」とは

「Reactの最適化」と聞いて、最初は思っていました:

メモ化を使ってすべてを高速化する。

今は違って見えています。

Reactパフォーマンスとは:

  • なぜコンポーネントが再レンダリングされるかを理解すること
  • 再レンダリングが問題ないケースを知ること
  • 不必要な処理を避けること(すべての処理ではなく)
  • まず予測可能で読みやすいコンポーネントを書くこと

Reactはデフォルトで十分速い。最適化とは恐怖ではなく、意図に基づくものです。


再レンダリングは敵ではない

大事なことを学んでいます: 再レンダリングは正常なことです。

コンポーネントが再レンダリングされるのは:

  • stateが変わったとき
  • propsが変わったとき
  • 親コンポーネントが再レンダリングされたとき
  • 使用しているContextが更新されたとき

これが起きたからといって、何かが間違っているわけではありません。

デフォルトでは、親が再レンダリングされるとすべての子も再レンダリングされます

Parent Component
   ├── Child A
   └── Child B

Parentのstateが変わると:

Parent Component  🔄
   ├── Child A     🔄
   └── Child B     🔄

Child AChild Bも更新されたstateを使っていなくても、再レンダリングされます。

これはReactの正常な動作——多くの場合、全く問題ありません。

Reactの再調整(reconciliation)は効率的で、再レンダリングのコストが低いことがほとんどです。 本当の問題は、重い計算や大きなコンポーネントツリーが不必要に再レンダリングされることです。

これを理解しただけで、最適化への考え方が変わりました。


React.memo:役立つが、魔法ではない

Child AReact.memoでラップしてみましょう。

Parent Component
   ├── Child A (memoized)
   └── Child B

Parentが再レンダリングされてもChild AのPropsが変わっていなければ

Parent Component  🔄
   ├── Child A     ⏸️ (スキップ)
   └── Child B     🔄

ここでReact.memoが役立ちます:

  • 不必要な処理を防ぐ
  • Propsが参照として等しい場合のみ有効

最初、React.memoは簡単なパフォーマンス向上の手段だと思っていました。

今わかること:

  • React.memoPropsが参照として等しい場合のみ再レンダリングを防ぐ
  • レンダリングのたびにPropsが変わる場合、何も起きない
  • 複雑さと比較コストが増す

シンプルな例:

const ItemList = React.memo(({ items }) => {
  return items.map(item => <Item key={item.id} {...item} />);
});

これが有効なのは、itemsが毎レンダリングで変わらない場合だけです。

実際の問題を特定してから使うようにしています——デフォルトでは使わない。


useMemoは値のため、速度のためではない

私が持っていた誤解: useMemoは処理を速くする。

今学んでいること:

  • useMemoをメモ化する
  • 重い計算に有効
  • 単純なロジックを魔法のように最適化はしない

例:

const sortedItems = useMemo(() => {
  return items.sort((a, b) => a.price - b.price);
}, [items]);

これが意味をなすのは:

  • 計算が重い場合
  • 依存値が安定している場合
  • メモ化した値が再利用される場合

どこにでもuseMemoを使うのはノイズを増やすだけです。

useMemoなし:

Render
 └── expensiveCalculation() 💸 毎回実行

useMemoあり:

Render
 ├── 依存値が変わった? → 再計算
 └── 依存値が同じ?    → キャッシュ値を再利用

視覚的に:

無関係なstateの変化
 └── useMemo値を再利用 ✅

これでuseMemoを盲目的に使わず、まず問い直すようになりました:

この計算は本当に重いか?


useCallbackがようやくわかった

useCallbackはずっと混乱していました。

今は理解が深まっています:

  • 関数の参照をメモ化する
  • メモ化された子コンポーネントにコールバックを渡すときに重要
  • 動作ではなく、同一性の問題
const handleClick = useCallback(() => {
  setCount(c => c + 1);
}, []);

React.memoでラップされたコンポーネントにhandleClickを渡すときに有効です。

そのコンテキストがなければ、useCallbackはほとんど役立たない——読みやすさを損なうことすらある。

よくある間違い(私も何度もやりました):

Parent
 └── Child (memoized)

でも親はレンダリングごとに新しい関数を渡している:

Parent再レンダリング
 └── onClick = () => {}  ❌ 新しい参照

結果:

Parent Component  🔄
   └── Child       🔄 (propsが変わった!)

useCallbackを使うと:

Parent再レンダリング
 └── onClick = useCallback(...) ✅ 同じ参照

結果:

Parent Component  🔄
   └── Child       ⏸️ (スキップ)

この図がuseCallbackを「わかった」瞬間でした: 👉 ロジックではなく、関数の同一性の問題なのです。


Keyとリスト:小さな詳細、大きな影響

見直している別の教訓:keyは思っていた以上に重要です

配列のインデックスをkeyにすると:

  • コンポーネントのstateが壊れる
  • 不必要な再レンダリングが起きる
  • 微妙なUIバグが生まれる

安定したユニークなkeyはReactが何が実際に変わったかを理解するのを助けます。

小さなことですが、パフォーマンスと正確性への影響は大きい。

インデックスをkeyとして使う場合:

変更前:
[ A, B, C ]
 0  1  2

Aを削除した後:
[ B, C ]
 0  1

Reactは認識する:

A → B
B → C

結果:

  • 間違ったコンポーネントが再利用される
  • stateのバグ
  • 余分な再レンダリング

安定したIDを使う場合:

変更前:
[ A(id=1), B(id=2), C(id=3) ]

Aを削除した後:
[ B(id=2), C(id=3) ]

Reactは正しく認識する:

Aが削除された
Bは変更なし
Cは変更なし

この小さな変更が多くの微妙なバグを防ぎます


自分が犯してきたミス

振り返ると、自分が犯してきたミスがいくつかあります——今でも時々やってしまいます:

  • 「念のため」useMemouseCallbackを使う
  • 何も計測せずに最適化する
  • パターンを理解せずにコピーする
  • React DevTools Profilerを無視する

どれも劇的なミスではありませんが、積み重なります。

ReactデベロッパーがuseMemoとuseCallbackをどこにでも追加して、それでも再レンダリングが起きている様子
新しいReactコンポーネントを作るときの私の気持ち

おわりに

Reactの最適化は、巧みなトリックやすごいフックについてではありません。

私にとってそれは:

  • Reactがどう考えるかを理解すること
  • シンプルで予測可能なコンポーネントを書くこと
  • 恐怖ではなく意図を持って最適化すること

この記事は今現在学んでいることのスナップショットに過ぎません——きっとあとで読み返したとき、新しい視点が加わっているはずです。

読んでくれてありがとうございます。これからも学び、作り、シェアし続けます 🙏

おすすめの記事