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.filter
や sort
を毎レンダー走らせるとフレーム落ちの原因になります。
3. JSX生成コスト削減
巨大リストを map で JSX に展開するとき、その戻り値をメモ化すると描画コストを下げられます(React 18の並列レンダーでも効果的)。
useCallbackとの違い
観点 | useMemo | useCallback |
---|---|---|
目的 | 値をメモ化 | 関数参照をメモ化 |
戻り値 | 計算結果そのもの | メモ化済みの関数 |
主要用途 | 高コストの計算結果を保持 | 子コンポーネントへ渡すハンドラの参照固定 |
内部実装では 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
が同一参照の間は再計算されません。
依存配列設計のコツ
- リストやオブジェクトは参照が変わる条件を把握
- Redux/Context でステートが再生成されると参照が毎回変わる ⇒ 先に
useSelector(shallowEqual)
などで固定化
- Redux/Context でステートが再生成されると参照が毎回変わる ⇒ 先に
- 不要な依存を追加しない
- 計算結果に影響しない値を含めると再実行が増える
- 入れ忘れはバグの温床
- ESLintの
react-hooks/exhaustive-deps
ルールを有効化し、自動検出する
- ESLintの
// NG: setState関数は安定参照のため依存不要
useMemo(() => compute(state, setState), [state, setState]);
// OK
useMemo(() => compute(state), [state]);
パフォーマンス測定と検証
Profiler API
- React DevTools を開き「Profiler」タブを選択
- 録画して操作 ⇒ 再計算が走る場所を特定
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
は
- 高コスト計算の結果をキャッシュし
- 依存配列が変わるまで再利用し
- 不要な再レンダーを抑制
というシンプルながら強力な最適化手段です。ただし「とりあえず入れる」は逆効果。まずはプロファイリングでボトルネックを特定し、効果が見込める箇所にのみ導入するのがベストプラクティスです。正しい依存配列と計算粒度を意識し、快適なReact UIを届けましょう。