再レンダリングを防ぐ!React のuseMemoとuseCallbackの要点

[PR]

React

Reactのアプリケーションでは、コンポーネントが意図せず頻繁に再レンダリングされることによってパフォーマンスが低下することがあります。特に値や関数の参照が毎回変わることが原因のひとつです。useMemoとuseCallbackはそれぞれ値と関数の再生成を制御し、不要な再レンダリングを抑えるための強力なツールです。この記事では、それらの使いどころや注意点を、最新情報に基づいて詳しく解説します。

React 再レンダリング 防ぐ useMemo useCallback の基本理解

Reactでは、状態や親コンポーネントのpropsが変わるとコンポーネントは再レンダリングされます。ただしそのすべてが視覚的変更を伴うわけではなく、関数やオブジェクトなどの参照(リファレンス)が毎回新しく生成されることで、不必要な再レンダリングが発生するケースが多くあります。useMemoとuseCallbackはその参照の安定性を保つために用いるフックです。useMemoは値のメモ化、useCallbackは関数のメモ化を行います。

この基本理解がなければ、どれだけツールを使いこなしても誤用や過剰最適化の原因となります。値や関数の参照が新しくなるたびにpropsとして渡されると、子コンポーネントはReact.memoを使っていても再レンダリングしてしまうので、両方を適切に組み合わせることが鍵です。

再レンダリングとは何か

再レンダリングとは、Reactがコンポーネントの関数を再び実行して仮想DOMを再評価し、それが実際のDOMの更新につながることを指します。状態(state)変更、親から渡されたpropsの変化、コンテキストの変更などがきっかけとなります。視覚上やユーザー体験上大きく変化しない部分でもこれらが起きると無駄な処理となります。

再レンダリング自体はReactの設計による正しい挙動ですが、無駄なものを減らすことでUIの応答性やアプリの全体的なパフォーマンスは向上します。特に大規模アプリや頻繁に更新があるインターフェースでは再レンダリング管理が重要です。

参照の安定性(referential stability)の重要性

オブジェクト・配列・関数などの値は、毎回新しく生成されると異なる参照になります。React.memoは浅い等価比較を行うため、参照が変わると再レンダリングを引き起こします。参照が変わらなければReact.memoは再レンダリングをスキップできます。

参照の安定性を保つためには、useMemoで値をメモ化し、useCallbackで関数をメモ化するのが標準的な方法です。不要なオブジェクトリテラルやインライン関数を避けることで、参照が変わるリスクを減らせます。

React.memoとの関係性

React.memoはコンポーネントをラップして、前回と同じprops(浅い比較)なら再レンダリングを省略します。この機能が働くためには、propsとして渡される値・関数の参照が毎回新しくならないことが条件です。useCallback・useMemoを併用することで、この前提を満たせるようになります。

React.memo単体だけでは、親コンポーネントが何かしらのstateを更新すると子コンポーネントも再評価されることがあり、実際にはpropsの参照が変わることが原因で再レンダリングが発生します。React.memoのメリットを最大化するためには、useCallback/useMemoとセットで使いこなすことが求められます。

useMemo の活用による再レンダリング防止と使いどころ

useMemoは、計算結果やオブジェクト・配列など再生成コストが高い値をメモ化し、依存変数が変わらない限り再計算を防ぐ手段です。レンダリング中の値の変換・フィルタリング・ソート等が典型例です。常に使えばよいわけではなく、あくまでコストとメリットを天秤にかけて適用するのが現在の推奨されています。

useMemoを正しく活用することで、多くの「無意味な再計算」を回避できます。たとえば大量のデータをフィルタリングしたり、設定オブジェクトを子コンポーネントへ渡す際などに参照を安定化させることで、子側が不必要にレンダリングされることを防げます。

値の計算コストが高い処理

大きな配列のフィルタリングやソート、複雑な数学処理、長時間かかる文字列操作など、CPU負荷が無視できない場合にuseMemoを使って計算結果をキャッシュすることが有効です。依存変数が頻繁に変わらない限り、大きな改善が期待できます。

ただし、値の計算が軽い場合や、レンダリングそのもののコストが小さいコンポーネントではuseMemoによるオーバーヘッドのほうが大きくなることもあるため、パフォーマンス計測ツールを使って判断することが重要です。

子コンポーネントへのオブジェクトや配列のprops渡し

親コンポーネントがオブジェクトや配列をインラインで生成して子に渡すと、毎回参照が変わるためReact.memoが効きません。useMemoでこれらをメモ化し、参照を安定化させることで子コンポーネントの不要な再レンダリングが防げます。

たとえば設定オブジェクトやスタイルオブジェクト、構成データなどを渡す際に、useMemoでラップすることで、依存が変わらない限り同じ参照を維持できます。これによりUIの更新コストを下げることができます。

依存配列の管理と落とし穴

useMemoを使う際には依存配列(dependencies)が正しく設定されているかが非常に重要です。不足があると古い値を参照し続けてバグの原因になりますし、過剰だと頻繁に再計算されてrender costが増えます。Lintツールや型システムを使ってミスを減らすのが良い方法です。

また、配列やオブジェクトを依存に含める場合、これらの参照安定性も考慮する必要があります。安定しないオブジェクトを依存にしてしまうと、useMemoの意味が薄れてしまいます。

useCallback を使った関数のメモ化で防ぐ再レンダリング

Reactでは関数も参照型であり、コンポーネント内で定義された関数は毎回新規インスタンスが生成されます。これが子コンポーネントへ渡されると、propsが変わったと判断されて再レンダリングされることがあります。useCallbackは関数参照を安定化させてこの動きを防ぎます。

ただし、すべての関数に使えば良いわけではなく、子に渡す関数、他のHookの依存として使われる関数など、使用される場面を限定することが最適化のポイントです。しかも関数が軽いならばオーバーヘッドの方が大きくなることもあります。

関数をpropsとして子コンポーネントに渡すケース

親から渡されるコールバック関数が子側の再レンダリングを引き起こす典型的な原因です。子コンポーネントがReact.memoでラップされていても、関数参照が毎回変わると判定が異なるため再レンダリングが発生します。useCallbackでその関数の参照を維持することで、子側の再レンダリングを抑えられます。

この方法は特に多くの子コンポーネントや頻繁にprops更新が起きる場面で有効です。ただし、stateやpropsに依存する関数では依存配列を正しく設定する必要があります。

useEffect や useMemo の依存配列で使われる関数

依存配列に関数を含める場合、その関数が毎回新しく生成されているとEffectやuseMemoが毎回再実行されてしまいます。useCallbackでラップして依存配列の関数参照を安定させることにより、EffectやuseMemoの実行回数を適切に制御できます。

ただし、Effectの中で関数を定義するなどで依存配列を減らす設計にすると、そもそも関数を依存に含める必要がなくなることもあります。これも最新の最適化パターンとして注目されています。

過剰使用と読みやすさのトレードオフ

useCallback をすべての関数に使うとコードが煩雑になり、依存配列の管理ミスや可読性の低下を招きます。必要な場所のみ選んで使うことが、最新の開発現場で推奨されているアプローチです。

実際に多くのエンジニアリングチームが、パフォーマンスプロファイルを取ってからuseCallbackやuseMemoを導入する判断をしています。見た目の最適化よりも測定に基づいたものが成果をもたらします。

React.memo と他の手法による再レンダリングの制御

React.memoは関数型コンポーネントに対してpropsが浅く等しい場合に再レンダリングをスキップさせる高階コンポーネントです。useMemoとuseCallbackと連携することで最大限機能しますが、それだけではカバーできないケースもあります。contextや頻繁に変わるpropsなどが原因になることがあります。

また、Reactの最新バージョンではコンパイラ最適化(React Compilerや自動メモ化機能)が強化されており、手動のmemoizationがある程度不要になる場面も増えています。ただし完全ではないため、知識と判断力は依然として重要です。

React.memo の使い方と注意点

React.memoを使うと、親のレンダリングが起きてもpropsが変わらなければ子を再レンダリングしません。ただし、渡すpropsにオブジェクト・配列・関数が含まれていてそれらの参照が毎回異なると判断されると意味がなくなります。arePropsEqualという比較関数をカスタマイズすることもできますが、比較処理自体が重くなる場合もあるので注意が必要です。

また、contextを使用している場合、Contextの値が新しくなれば消費しているコンポーネントは再レンダリングします。contextを細かく分割するか、消費側で不必要な値を避ける設計が効果的です。

ローカルステートとステートのリフトアップの影響

ステートが深い階層や高レベルな親で管理されていると、そのステートが変わるたびに多くの子コンポーネントが再レンダリングされがちです。ステートをできるだけ必要な場所に近づけ、リフトアップはあまりに広範囲でないように設計すると再レンダリングを減らせます。

また、ステートの境界を明確にし、グローバルなステート管理を使う際も、更新が必要な部分だけを更新するように工夫することで無駄な再描画を防げます。Reactの最新バージョンでもこの設計パターンは重視されています。

Profiler やレンダリング計測の活用

どの部分がボトルネックかを見つけるために、React DevToolsのProfilerやベンチマークツールを使って実際に再レンダリングの発生回数や時間を計測することが最初のステップです。感覚や仮定だけで最適化を始めると、過剰または間違った対応をしてしまいます。

計測で特定の子コンポーネントや関数が頻繁に再生成されていることがわかれば、useMemo/useCallbackそのものを導入するか、コード構造を見直すかの判断がつきやすくなります。

パフォーマンス最適化の最新動向と実践例

Reactの最新バージョンでは、コンパイラによる最適化や自動メモ化機能が強化されており、useMemoやuseCallback、React.memoの手作業による適用をある程度代替できる機能があります。しかし実践的には、手動でのmemoizationが依然として多くの現場で必要とされています。特にブラウザ条件や端末能力が低い環境では手間をかけた最適化の効果が体感できることが多いです。

また、業務用のダッシュボードやデータ可視化、多数のUIコンポーネントが動的に並ぶアプリケーションなどでは、useMemoとuseCallbackの組み合わせが劇的な差を生みます。コードベースが大きいほど、過剰レンダリングのコストは無視できなくなるからです。

コンパイラ最適化の影響

Reactのコンパイラには、コンポーネント自体の最適化や再レンダリングの自動検出機能が含まれており、手動でReact.memoをラップしなくても不要な再描画を抑えることが可能な場面が増えています。この機能の恩恵を受けるためには、Reactの最新バージョンを利用し、最新のビルド設定を理解しておくことが重要です。

ただしこの機能はすべてを自動で処理するわけではなく、関数を渡すpropsや値の参照安定性など、手動で設計・改善するところが残っています。特定の状況では依然として手動のmemoizationが必要です。

典型的な実践例

たとえば、データの一覧表示コンポーネントやフィルタ付きリスト、チャート、フォームなどでは、フィルタリングや設定オブジェクトの生成、イベントハンドラを子に渡すケースが頻繁に登場します。これらをuseMemo/useCallbackでラップし、React.memoで子を包む組み合わせが多くのアプリでパラメータ更新時のモーダルやリスト再描画を抑える効果を出しています。

また、複数のフックスで依存配列が複雑になる場所では、関数定義の位置を見直して依存自体を減らす設計を採用するプロジェクトも増えています。不要な依存を減らすことでuseMemo/useCallbackの安定性が上がります。

アンチパターンと注意例

使用例としては、useMemoを使っていない軽い処理に対してまでメモ化を行うこと、useCallbackを用いてもその関数を渡す子がReact.memoでないこと、また依存配列が適切でなくバグや誤動作を招くことなどが挙げられます。過剰最適化は可読性を低下させたり、予期しない副作用をもたらすこともあります。

特に依存配列にオブジェクトや配列そのものを入れていると、それらが毎回新しい参照を持つならばuseMemo/useCallbackのメリットを失います。設計段階で参照の安定性を意識することが最新の実務で重視されています。

まとめ

Reactで再レンダリングを防ぐためには、再レンダリングのトリガーと参照の不変性を理解することが出発点です。useMemoは値やオブジェクトの計算結果をメモ化し、useCallbackはコールバック関数そのものの参照を安定化させます。それぞれが適切な場所で使われると、特にReact.memoとの組合せで子コンポーネントの不必要な再レンダリングを削減できます。

ただし最新のReactではコンパイラによる自動最適化が強化されており、手動のmemo化は場合に応じて使い分けることが標準になっています。過剰な最適化は複雑さとリスクを伴うため、Profilerで計測しながら必要な部分から導入することが最も賢明です。

特集記事

コメント

この記事へのトラックバックはありません。

TOP
CLOSE