Featured image of post ReactのuseMemoとuseCallbackの過剰な適用を防ぐ基準 Featured image of post ReactのuseMemoとuseCallbackの過剰な適用を防ぐ基準

ReactのuseMemoとuseCallbackの過剰な適用を防ぐ基準

Reactコンポーネントの最適化で多用されがちなuseMemoとuseCallbackについて、本来必要なケースと、過剰適用がパフォーマンスを悪化させる原因を分析します。

はじめに

React アプリケーションのパフォーマンス最適化において、多くの開発者が真っ先に行うのが useMemouseCallback の導入です。

「関数の生成や計算処理をキャッシュ(メモ化)してくれるなら、手当たり次第にすべてのオブジェクトや関数を囲んでおけば動作が早くなるのでは?」と勘違いしてしまいがちですが、実際にはこれは大きな間違いです。

不要なメモ化は、コードの可読性を下げるだけでなく、依存関係配列の比較コストやキャッシュデータ保持用のメモリのオーバーヘッドにより、かえってアプリのパフォーマンスを低下させる原因になります。

本記事では、これら2つのフックを「使うべき場面」と「使うべきでない場面」の明確な判断基準を整理します。


1. メモ化が抱える隠れたコスト

useMemouseCallback は、決して「無料(タダ)」の処理ではありません。以下のコストがレンダリングのたびに発生します。

  1. 依存関係の比較コスト: Reactは、レンダリングごとに依存関係配列(Deps)の中身を取り出し、前回の値と浅い比較(Object.is)を実行します。この比較処理自体がCPU負荷になります。
  2. メモリの追加消費: 前回の関数の参照や、計算した戻り値をメモリ上に保持(キャッシュ)し続けるため、ガベージコレクション(GC)の対象にならずメモリフットプリントが増加します。
  3. コードの複雑化: 依存関係の指定ミスによるバグ(古い値を参照したまま画面が更新されないクロージャ問題など)を引き起こしやすくなります。

2. useMemo を使うべき明確な基準

useMemo を適用してよいのは、以下の2つのケースに限られます。

① 計算コストが非常に高い処理(CPUバウンド)

配列のソート、大量のデータのフィルタリング、複雑なデータマッピングなど、ミリ秒単位で処理時間がかかる計算が含まれる場合です。

  • 良い例:
    // 1万件のデータを検索条件でフィルタリングする重い処理
    const filteredItems = useMemo(() => {
      return heavySearch(items, query);
    }, [items, query]);
    

② 他のコンポーネントの React.memo の依存キーになるオブジェクト

Reactはコンポーネントに渡されるオブジェクトが「同じ内容」であっても、レンダリングのたびに別個の参照オブジェクト(別のアドレス)として生成し直します。

これを防止し、子の不要な再描画を防ぐためにはオブジェクトの参照を固定する必要があります。

  • 良い例:
    // オブジェクトの参照を固定する
    const options = useMemo(() => ({ color: themeColor }), [themeColor]);
    
    // 子要素が React.memo 化されている場合、optionsの参照が変わらないため再描画を防げる
    return <HeavyChildComponent options={options} />;
    

3. useCallback を使うべき明確な基準

useCallback の役割は、関数の再生成を防ぐことではなく、**「関数の参照(メモリ上の位置)を固定すること」**です。

よって、以下のケースでのみ使用します。

① React.memo化された子コンポーネントに関数をPropsとして渡す場合

親コンポーネントが再レンダリングされると、通常は関数も新しく生成し直されます。そうすると、子コンポーネントに渡る関数の「参照」が変わってしまうため、子コンポーネント側で React.memo を適用していても強制的に再描画されてしまいます。

これを防ぐためには、関数の参照を useCallback で固定します。

  • 良い例:

    // 関数の参照を固定
    const handleClick = useCallback(() => {
      console.log("Clicked!");
    }, []);
    
    // 子が React.memo されている場合に効果を発揮する
    return <MemorizedButton onClick={handleClick} />;
    
  • 注意 (重要): もし渡す先の子コンポーネントが普通の(React.memo されていない)コンポーネントであるなら、どれほど親で関数を useCallback で囲んでいても、子コンポーネントは親のレンダリングと同時に必ず再描画されます。 この場合の useCallback は全くの無駄(余計なコスト)になります。


まとめ:React 19と今後の展望

React 19以降では、コンパイラ(React Compiler)による「自動的なメモ化」のサポートが導入されています。

これにより、開発者が手動で useMemouseCallback を書かなくても、ビルド時に自動的に適切な箇所が最適化される時代へとシフトしつつあります。

手動でのメモ化にあたっては、

  1. 「本当にミリ秒単位で遅い計算なのか」を計測(Profile)して確認する
  2. 子の再描画防止(React.memo との連携)以外の理由で関数やオブジェクトを無闇に囲まない
  3. 可能な限り純粋なJavaScriptの書き方でロジックをシンプルに保つ

この3原則を意識して、必要十分なコード設計を心がけましょう。