useMemo徹底ガイド──Reactアプリの再計算コストを劇的に減らす最適化テクニック

React

useMemoとは何か

useMemo は指定した計算式をメモ化(キャッシュ)し、依存配列が変わらない限り再実行をスキップするReactフックです。たとえば数千件のリストを高コストなフィルタリングやソートで描画する場合、入力が変わらなければ同じ結果を再計算するのはムダです。useMemo を使えば計算を一度だけ行い、次回レンダーではキャッシュ済みの値を返すため描画が高速化します。

import { useMemo } from 'react';

function ExpensiveList({ items, query }) {
  const filtered = useMemo(() => {
    return items.filter(item => item.name.includes(query));
  }, [items, query]); // itemsかqueryが変わったときだけ再計算

  return filtered.map(item => <li key={item.id}>{item.name}</li>);
}

基本シグネチャ

const memoizedValue = useMemo(() => compute(expensive), [deps]);
  • 第1引数:メモ化したい関数(副作用を含まない純粋関数が望ましい)。
  • 第2引数:依存配列。ここに列挙した値が変わった場合のみ計算し直す。

依存配列を空配列 [] にするとマウント時に一回だけ実行。undefined を渡すと毎レンダー実行=メモ化無効なので注意。

useMemoが効く典型シナリオ

1. データ変換・重い計算

  • 数学的シミュレーションの結果
  • JSONデータのディープコピーや正規化
  • Markdown → HTML 変換など CPUコストが高い処理

2. フィルタ・ソート・検索

リストの要素数が数百〜数千を超える場合、Array.prototype.filtersort を毎レンダー走らせるとフレーム落ちの原因になります。

3. JSX生成コスト削減

巨大リストを map で JSX に展開するとき、その戻り値をメモ化すると描画コストを下げられます(React 18の並列レンダーでも効果的)。

useCallbackとの違い

観点useMemouseCallback
目的をメモ化関数参照をメモ化
戻り値計算結果そのものメモ化済みの関数
主要用途高コストの計算結果を保持子コンポーネントへ渡すハンドラの参照固定

内部実装では useCallback(fn, deps)useMemo(() => fn, deps) と同等です。しかし意味づけが異なるため、値なら useMemo、関数なら useCallback を使うと読みやすくなります。

実践シナリオ別コード例

例1:検索フィルタリング

function SearchableTable({ rows }) {
  const [keyword, setKeyword] = useState('');
  const visibleRows = useMemo(() => {
    const lower = keyword.toLowerCase();
    return rows.filter(r => r.name.toLowerCase().includes(lower));
  }, [rows, keyword]);
  return (
    <>
      <input value={keyword} onChange={e => setKeyword(e.target.value)} />
      <ul>{visibleRows.map(r => <li key={r.id}>{r.name}</li>)}</ul>
    </>
  );
}
  • rows が巨大でも keyword が変わらなければ再計算なし
  • 入力ごとにフィルタされても実用レベルのパフォーマンス

例2:高コスト画像処理

function Preview({ file }) {
  const thumbnail = useMemo(() => {
    return file ? createThumbnail(file) : null; // Canvas処理など重い関数
  }, [file]);
  return thumbnail ? <img src={thumbnail} /> : null;
}

例3:ソート済みキャッシュ

const sorted = useMemo(
  () => [...items].sort((a, b) => a.date - b.date),
  [items]
);

イミュータブルデータならシャローコピーしてソートし、items が同一参照の間は再計算されません。

依存配列設計のコツ

  1. リストやオブジェクトは参照が変わる条件を把握
    • Redux/Context でステートが再生成されると参照が毎回変わる ⇒ 先にuseSelector(shallowEqual)などで固定化
  2. 不要な依存を追加しない
    • 計算結果に影響しない値を含めると再実行が増える
  3. 入れ忘れはバグの温床
    • ESLintのreact-hooks/exhaustive-depsルールを有効化し、自動検出する
// NG: setState関数は安定参照のため依存不要
useMemo(() => compute(state, setState), [state, setState]); 
// OK
useMemo(() => compute(state), [state]);

パフォーマンス測定と検証

Profiler API

  1. React DevTools を開き「Profiler」タブを選択
  2. 録画して操作 ⇒ 再計算が走る場所を特定
  3. useMemo導入後にレンダー時間が短縮されているか確認

console.time計測

const result = useMemo(() => {
  console.time('expensive');
  const v = heavy();
  console.timeEnd('expensive');
  return v;
}, [deps]);

メモ化の前後で時間を比較し、効果を数字で把握します。

よくある落とし穴

症状原因対策
メモ化しているのに速くならない計算自体が軽い/依存が毎回変わるメモ化を外す or 依存を安定化
stale data(古い値を参照)依存配列に必要な変数がないESLintルールで検出+修正
メモリリーク大きな配列・Mapをキャッシュして解放しない必要に応じて useMemo を解除

useMemoを使わない方がいい場合

  • 計算コスト < メモ化管理コスト
  • UIがほぼ静的:再レンダー回数が少ない
  • 依存が頻繁に変わる:キャッシュがほぼ活きない

最適化は「必要になったら測定して導入」が鉄則です。

useMemo と Suspense / Concurrent Features

React 18 の並列レンダーでは、useMemoのキャッシュが中断と再開をまたいで保持されるため、長い計算でもUIスレッドをブロックしにくくなっています。ただしメモ化対象関数は同期的に実行されるため、CPU負荷の高い処理はWeb WorkerやuseTransitionと組み合わせるとさらに快適です。

まとめ

useMemo

  1. 高コスト計算の結果をキャッシュ
  2. 依存配列が変わるまで再利用
  3. 不要な再レンダーを抑制

というシンプルながら強力な最適化手段です。ただし「とりあえず入れる」は逆効果。まずはプロファイリングでボトルネックを特定し、効果が見込める箇所にのみ導入するのがベストプラクティスです。正しい依存配列と計算粒度を意識し、快適なReact UIを届けましょう。

コメント