ReactのuseCallback・useMemoの過剰使用とパフォーマンス逆効果の検証
React Frontend Performance JavaScript
結論
無暗なメモ化は Object.is 比較コストで逆効果です。重い計算と React.memo 連携のみに使用します。
// ❌ 逆効果:軽量計算を useMemo で囲む悪手(比較コストの方が高い)
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
// ⭕ 正解:単純な文字列結合やフィルタはメモ化せず直接計算
const fullName = `${firstName} ${lastName}`;
React公式仕様:メモ化が持つ隠れたコスト
React公式ドキュメント(react.dev/reference/react/useMemo)で明記されている通り、useMemo や useCallback は無料の最適化ではありません。
フックを呼び出すたびに、React内部では以下のオーバーヘッドが発生します:
- 依存配列(deps)の
Object.isによる全要素比較コスト - クロージャ関数および前回の結果値をメモリへ保持するコスト
数ミリ秒未満で完了する軽量なデータ加工や関数生成に対してメモ化を行うと、「本来の計算コスト」よりも「フックの依存比較コスト」の方が大きくなり、コンポーネントのレンダリング速度が低下する逆効果事故 が発生します。
メモ化の正当な使いどころ 3原則
| 適用条件 | 理由と効果 |
|---|---|
| 1. 重い計算(1ms以上) | 数千件の配列ソートや正規表現解析など、再計算をスキップする価値がある処理 |
2. React.memo 子要素への参照保持 | 子コンポーネントが React.memo で保護されており、関数の参照同一性を保ちたい時 |
3. useEffect 依存配列への指定 | フックの再実行ルールを制御するため、関数のインスタンス固定が必要な時 |
実際に起こるトラブル:間違ったメモ化による保守性低下
// ❌ ダメな例:React.memo を使っていない普通の子コンポーネントに関数を渡す場面での useCallback
function Parent() {
const [count, setCount] = useState(0);
// 普通のButtonコンポーネント(React.memoなし)に渡す場合、useCallback は一切意味を持たない
const handleClick = useCallback(() => console.log('click'), []);
return <NormalButton onClick={handleClick} />;
}
NormalButton が React.memo で囲まれていない場合、親がレンダリングされれば handleClick の参照が固定されていようと NormalButton は必ず再レンダリングされます。意味のない useCallback はコードを複雑にし、メモリを無駄に消費するだけです。
改善手順
- まずメモ化(
useCallback/useMemo)をすべて外したシンプルな状態でコードを書く - React DevTools Profiler で実際のレンダリング時間(16ms超過の有無)を計測する
React.memoを適用した子要素へのプロパティ伝播箇所のみに限定してメモ化を導入する