「useMemoとuseCallbackって、結局なにが違うの?」「とりあえず全部useMemoで囲んでおけば速くなる?」――Reactで開発していると、誰もが一度はぶつかる疑問です。本記事では、両者の本質的な違いから、いつ・どう使い分けるべきか、そしてやらかしがちな落とし穴まで、現役Reactエンジニア視点で徹底的に解説します。サンプルコード10本超・比較表・パフォーマンス測定の実例を交えながら、「メモ化したのに再レンダリングが止まらない」「Profilerで計測したら逆に遅くなっていた」といった現場のリアルにも踏み込みます。さらに2025年に正式リリースされたReact Compilerの存在によって変わりつつある「手動メモ化の現在地」も整理し、2026年時点のベストプラクティスを提示します。読み終えるころには、useMemo / useCallback / React.memo / forwardRef / lazy / Suspense の関係性が一本の線でつながり、ProfilerやReact DevToolsを使った計測ベースの最適化判断ができるようになっているはずです。
useMemoとuseCallbackは何のために存在するのか
まず大前提として、useMemoもuseCallbackも「Reactの再レンダリング時に同じ参照を保ち続けるためのHook」です。「計算を速くするための魔法」ではありません。この一文を腹落ちさせるかどうかで、最適化の精度が劇的に変わります。Reactは状態が更新されるたびに関数コンポーネントを再実行するので、その内部で生成される値や関数は、デフォルトでは毎回新しい参照になります。新しい参照は、依存配列を持つuseEffectや、React.memoでラップされた子コンポーネントにとっては「変化した」と認識され、不要な処理を引き起こす原因になります。
関数コンポーネントの再実行と参照の問題
関数コンポーネントは、自分の状態が変わったとき、親が再レンダリングされたとき、Contextの値が変わったときに再実行されます。問題は、再実行のたびに以下のような等価だが参照が異なるオブジェクトが生まれてしまうことです。
function Parent() {
const [count, setCount] = useState(0);
// countが変わるたびに、handlerもfilterも「新しい関数・新しい配列」になる
const handler = () => console.log("click");
const filter = { type: "active", limit: 10 };
return <Child onClick={handler} filter={filter} />;
}
このコードでは、ChildがReact.memoでメモ化されていても、handlerとfilterの参照が毎回変わるため、props比較ですり抜けて再レンダリングが発生します。これを防ぐためのツールがuseMemoとuseCallbackです。
useMemoは「値」を、useCallbackは「関数」をメモ化する
ざっくり言えばuseMemoは計算結果(値)を、useCallbackは関数定義そのものをメモ化します。両者は実は同じ概念の別表現で、useCallback(fn, deps)はuseMemo(() => fn, deps)と等価です。
// この2つは等価
const memoFn1 = useCallback(() => doSomething(id), [id]);
const memoFn2 = useMemo(() => () => doSomething(id), [id]);
つまり「useCallbackはuseMemoのシンタックスシュガー」と理解しても実害はありません。ただし読みやすさ・意図の伝わりやすさが大きく違うため、用途に応じて使い分けます。
React.memoとセットで初めて意味を持つ
useCallbackで関数の参照を固定しても、受け取る子コンポーネントがReact.memoでラップされていなければ無意味です。親が再レンダリングすれば子も再レンダリングされるからです。これがuseMemo/useCallbackの「最初の落とし穴」です。
useMemoとuseCallbackの違いを徹底比較
ここで両者を一枚絵で整理しておきましょう。シグネチャ・返り値・主な用途・等価表現を並べて見ると違いが鮮明になります。
機能比較表
| 観点 | useMemo | useCallback |
|---|---|---|
| シグネチャ | useMemo(factory, deps) |
useCallback(fn, deps) |
| 返り値 | factoryの実行結果(値) | 関数そのもの |
| 主な対象 | 配列・オブジェクト・計算結果 | イベントハンドラ・コールバック |
| 等価表現 | ― | useMemo(() => fn, deps) |
| 再計算タイミング | depsが変わったとき | depsが変わったとき |
| 典型ユースケース | 重い計算のキャッシュ・参照固定 | React.memo子へのprops固定 |
| React Compilerでの扱い | 自動メモ化対象 | 自動メモ化対象 |
useMemoの基本構文と挙動
import { useMemo } from "react";
function ProductList({ items, keyword }) {
// keywordとitemsが変わらない限り、再計算されない
const filtered = useMemo(() => {
return items.filter((i) => i.name.includes(keyword));
}, [items, keyword]);
return (
<ul>
{filtered.map((i) => (
<li key={i.id}>{i.name}</li>
))}
</ul>
);
}
useMemoはfactory関数の実行結果をキャッシュし、depsが変わらなければ前回の値をそのまま返します。「計算量が大きい処理」または「参照を子に渡したい配列・オブジェクト」に使うのが基本です。
useCallbackの基本構文と挙動
import { useCallback, useState } from "react";
import { ChildButton } from "./ChildButton"; // React.memoでラップ済み
function ParentForm() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount((c) => c + 1);
}, []); // 関数本体がstateを直接参照しないので依存配列は空でOK
return <ChildButton onClick={handleClick} label={`count: ${count}`} />;
}
useCallbackは関数の参照そのものを記憶し、依存配列が変わらない限り同じ関数オブジェクトを返します。setStateの関数更新形式(setCount((c) => c + 1))と組み合わせると、依存配列を空にできて再生成が完全に止まる、というのが定番テクニックです。
関数の中身が同じでも、参照は別物
const a = () => 1;
const b = () => 1;
console.log(a === b); // false : 中身が同じでも別オブジェクト
JavaScriptは関数・配列・オブジェクトを参照で比較します。React.memoは浅い比較(Object.is)でpropsを判定するため、中身が同じでも参照が違えば「変わった」とみなします。ここがuseCallback/useMemoの出番です。
具体的なユースケース別の使い分け
「結局どっちを使うべきか」は文脈次第です。代表的な5つのケースを見ていきましょう。
ケース1: React.memoの子に関数を渡す
もっとも教科書的なケースです。子コンポーネントがReact.memoでラップされており、props経由でハンドラを受け取る場合、useCallbackで関数の参照を固定しないと、親の再レンダリングごとに子も再レンダリングされてmemoが無効化されます。
const TodoItem = React.memo(function TodoItem({ todo, onToggle }) {
console.log("render:", todo.id);
return (
<li onClick={() => onToggle(todo.id)}>
{todo.done ? "✓" : "□"} {todo.text}
</li>
);
});
function TodoList({ todos }) {
const [list, setList] = useState(todos);
// useCallbackで包まないと、TodoItemのmemoが効かない
const handleToggle = useCallback((id) => {
setList((prev) =>
prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
);
}, []);
return <ul>{list.map((t) => <TodoItem key={t.id} todo={t} onToggle={handleToggle} />)}</ul>;
}
ケース2: useEffectの依存配列に関数を入れたい
カスタムフックの中でfetch関数を返したり、useEffectの依存配列に関数を渡すケースです。useCallbackで包まないと、毎回新しい関数が生成されてuseEffectが無限ループになります。
function useUser(userId) {
const [user, setUser] = useState(null);
// useCallbackがないと、refetchの参照が毎回変わってeffectが無限実行される
const refetch = useCallback(async () => {
const res = await fetch(`/api/users/${userId}`);
setUser(await res.json());
}, [userId]);
useEffect(() => { refetch(); }, [refetch]);
return { user, refetch };
}
ケース3: 重い計算結果をキャッシュしたい
大量データの集計やソート、正規表現コンパイル、Date処理など、ミリ秒単位で時間がかかる処理はuseMemoの定番です。
function StatsView({ logs }) {
const stats = useMemo(() => {
// 10万件のログから集計
return logs.reduce((acc, log) => {
acc[log.level] = (acc[log.level] || 0) + 1;
return acc;
}, {});
}, [logs]);
return <pre>{JSON.stringify(stats, null, 2)}</pre>;
}
ケース4: 子に渡すオブジェクト/配列の参照を固定したい
React.memoの子にオブジェクトリテラルや配列リテラルを直接渡すと、毎回新しい参照になってmemoが効きません。useMemoで包みます。
function Page() {
const [query, setQuery] = useState("");
// queryが変わったときだけ新しいオブジェクトになる
const searchOptions = useMemo(
() => ({ keyword: query, limit: 20, includeArchived: false }),
[query]
);
return <ResultList options={searchOptions} />; // ResultListはReact.memo
}
ケース5: Context値の参照を固定したい
Context.Providerに渡すvalueは、参照が変わるたびにそのContextを購読している全コンポーネントが再レンダリングされます。複合値ならuseMemoで包むのがほぼ必須です。
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(() => ({ user, login: setUser, logout: () => setUser(null) }), [user]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
パフォーマンス測定:本当に速くなるのか
「メモ化=速くなる」は迷信です。ここではReact DevTools Profilerで計測した実測値ベースで、効くケースと効かないケースを示します。
計測対象のサンプル
500件のTodoリストをレンダリングするコンポーネントを、(A)何もメモ化しない (B)React.memoだけ (C)React.memo + useCallback の3パターンで比較しました。親で無関係なカウンターを1秒ごとにインクリメントし、子の再レンダリング時間を計測しています。
| パターン | 子コンポーネント描画時間 | フレーム落ち | 所感 |
|---|---|---|---|
| (A) メモ化なし | 約 28ms / tick | 頻発 | 500件×毎秒で明確な体感遅延 |
| (B) React.memoのみ | 約 27ms / tick | 頻発 | handlerの参照が変わるため効果なし |
| (C) React.memo + useCallback | < 1ms / tick | なし | 子は完全にスキップされる |
つまりuseCallback単体では効果がなく、React.memoと組み合わせて初めて意味を持つことが数値ではっきり出ます。
逆に遅くなるケース
useMemoには「比較・キャッシュ管理のオーバーヘッド」があります。計算が軽量な場合、メモ化のコストの方が大きくなることがあります。
// アンチパターン: 計算が軽すぎる
const doubled = useMemo(() => n * 2, [n]); // メモ化コストの方が高い
// シンプルに書けばよい
const doubled = n * 2;
計測の正しい順序
- まず動くものを作る(最適化なし)
- React DevTools Profilerで実測する
- ボトルネックの子コンポーネントを特定する
- その箇所にReact.memo + useCallback / useMemoを適用する
- Profilerで再計測して効果を確認する
この順序を守らないと、コードが汚れただけで何も速くなっていない、という結末になりがちです。
よくある落とし穴とアンチパターン
useMemoとuseCallbackは正しく使えば強力ですが、誤用は珍しくありません。現場で頻発する5つのアンチパターンを紹介します。
落とし穴1: 依存配列の漏れ(stale closure)
依存配列に必要な変数を含めないと、関数内で参照される値が古いまま固定され、いわゆる古いクロージャ問題が起きます。
// 危険: countが依存に入っていない
const increment = useCallback(() => setCount(count + 1), []);
// 修正版1: 依存にcountを入れる
const increment = useCallback(() => setCount(count + 1), [count]);
// 修正版2(推奨): 関数更新形式
const increment = useCallback(() => setCount((c) => c + 1), []);
ESLintのreact-hooks/exhaustive-depsルールを必ず有効化しましょう。エディタが警告してくれます。
落とし穴2: とりあえず全部useMemoで囲む
「念のため」とすべての変数をuseMemoで包むのは典型的な過剰最適化です。useMemo自体にコストがあるため、軽量な計算では純粋なオーバーヘッドになります。
落とし穴3: React.memoなしでuseCallback
子がReact.memoでラップされていないコンポーネントにuseCallbackで関数を渡しても、子は親と同時に再レンダリングされるため何の意味もありません。useCallbackはReact.memoとセットと覚えましょう。
落とし穴4: depsに「参照不安定なもの」を入れる
// 危険: 毎回新しいオブジェクトがdepsに入る
const result = useMemo(() => compute(opts), [{ ...opts }]);
// 正しい: optsそのものを渡す
const result = useMemo(() => compute(opts), [opts]);
毎回新しい配列・オブジェクトを依存配列に渡してしまうと、メモ化は実質的に効きません。依存配列に入れていいのは、参照が安定したプリミティブまたはメモ化済みの値だけです。
落とし穴5: 副作用をfactory内に書く
// アンチパターン: useMemoのfactoryでログ送信
const value = useMemo(() => {
fetch("/log", { method: "POST" }); // ❌ 副作用
return heavyCalc(input);
}, [input]);
useMemoのfactoryは純粋関数であるべきです。React 18以降のStrict Modeでは開発時に意図的に2回呼ばれることがあり、副作用が二重に起きてしまいます。副作用はuseEffectへ。
useMemo/useCallback以外の代替手段
2025〜2026年のReactエコシステムでは、メモ化の選択肢が増えています。useMemo/useCallbackだけが解ではありません。
React Compilerによる自動メモ化
2025年に正式リリースされたReact Compilerは、ビルド時にコンポーネントを解析し、必要な箇所に自動的にメモ化を適用します。これにより、開発者が手動でuseMemo/useCallbackを書く必要性が大きく減りました。プロジェクトに導入すれば、上で紹介した「とりあえず包む」問題から解放されます。ただしRules of Hooksに準拠したコードであることが前提で、ESLintプラグインeslint-plugin-react-compilerでの検証が推奨されます。
状態を持ち上げない設計(コンポジション)
そもそも親の状態を子に降ろさず、childrenとして合成すると、親の再レンダリング時に子は再評価されません。propsを減らせばメモ化の必要性自体が消えます。
function Layout({ children }) {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
{children} {/* childrenはcountの変化で再レンダリングされない */}
</div>
);
}
状態管理ライブラリでの分割購読
Zustand・Jotai・Redux Toolkitなどのライブラリは、セレクタ単位での購読を提供します。関心のあるstateだけを購読すれば、無関係な変更で再レンダリングされません。useMemoで頑張るより、ストア側で分割した方が効果的なケースは多いです。
関連するReact API
- React.memo: コンポーネントのprops浅比較ベースのメモ化
- forwardRef: ref転送。memoとの併用で内部refも安定化
- lazy / Suspense: コードスプリッティングによる初期描画コスト削減
- useTransition / useDeferredValue: 描画優先度を下げて重い更新を後回し
- useSyncExternalStore: 外部ストア購読の標準API
これらは「メモ化の代替・補完」として位置付け、ProfilerやReact DevToolsで計測しながら使い分けます。
2026年のベストプラクティス
ここまでを踏まえた、2026年時点での実践的な指針をまとめます。
判断フロー
- まずReact Compilerが導入できるか確認する。導入できれば手動メモ化はほぼ不要。
- Compilerが使えない・段階導入中なら、React DevTools Profilerで計測してボトルネックを特定。
- 子がReact.memo + propsに関数/オブジェクトを渡す構造なら、useCallback/useMemoをその箇所だけに適用。
- Context.Providerのvalueは複合値なら原則useMemoで包む。
- 状態管理ライブラリのセレクタやコンポジション設計で、そもそもメモ化が要らない構造を目指す。
必ず守る3つのルール
- ESLintで
react-hooks/exhaustive-depsを必ず有効化 - useMemo/useCallbackのfactory・関数は純粋に保つ(副作用禁止)
- 最適化前後でProfilerの数値を比較。改善がなければロールバック
チーム開発でのレビュー観点
| 観点 | チェック内容 |
|---|---|
| 必要性 | そのメモ化はProfilerで裏付けられた根拠があるか |
| 依存配列 | ESLintルールに従っているか / 不安定な参照を入れていないか |
| 副作用 | factory/関数が純粋か |
| セット運用 | useCallbackがReact.memoとセットになっているか |
| 代替検討 | コンポジション・状態管理ライブラリで解決できないか |
関連記事:Reactを深く理解するための基礎
useMemo/useCallbackを真に使いこなすには、HookやTypeScript・JavaScriptの基礎理解が欠かせません。以下の記事も合わせてどうぞ。
- React Hooks 完全実践ガイド〜useState・useEffect・カスタムフック・React 18新Hooks【2026年版】〜 — Hooks全体の関係性を整理
- TypeScript 完全実践ガイド〜型システム・ジェネリクス・ユーティリティ型・React+TypeScript【2026年版】〜 — Hooksに型を付ける実践
- JavaScript ベストプラクティス10選〜ES2025対応・現役エンジニアが選ぶ実践的ノウハウ【2026年版】〜 — 参照と等価性の基礎理解
キャリア視点:パフォーマンス最適化が評価される現場
Reactのパフォーマンス最適化スキルは、フロントエンドエンジニアの市場価値を直接押し上げます。とくにReact CompilerやReact 19以降の新機能を扱える人材は不足しており、未経験〜中級者でも体系的に学べばキャリアアップに直結します。
- テックアカデミー: Reactコースで実プロジェクト形式の学習が可能。現役エンジニアのメンタリング付き。
- 侍エンジニア: マンツーマンでオリジナルポートフォリオを作成。最適化やProfiler活用も指導範囲に含まれることが多い。
- レバテック: 中級者以降のフロントエンドエンジニア向け案件が豊富。React + TypeScript経験者は高単価が狙いやすい。
- Geekly: Web系自社開発企業の求人が中心で、モダンフロントエンド経験者の年収レンジが高め。
「ProfilerでReactアプリのボトルネックを特定し、Compiler導入とコンポジション設計で改善した」といった経験は、職務経歴書で強い武器になります。
FAQ:現場で本当によく聞かれる7つの質問
Q1. useMemoとuseCallback、どちらか1つだけ覚えるなら?
A. useMemoだけ覚えればよいです。useCallback(fn, deps)はuseMemo(() => fn, deps)と等価なので、useMemoが理解できればuseCallbackも自動的に理解できます。実務では可読性のためにuseCallbackを使うシーンが多いというだけです。
Q2. React Compilerを入れたらuseMemo/useCallbackは完全に不要?
A. 多くのケースで不要になりますが、サードパーティライブラリとの境界や、Compilerが解析を諦めるパターン(動的なRules of Hooks違反など)では引き続き必要です。「自分で書く頻度が激減する」という理解が正確です。
Q3. useEffectの依存配列に関数を入れるのが面倒です。どうすれば?
A. その関数をuseCallbackで包むか、関数自体をuseEffectの内側に閉じ込めましょう。effect内に閉じ込められれば依存に含める必要がなくなります。
Q4. React.memoだけで十分なケースは?
A. propsがプリミティブ(string/number/booleanなど)だけのときです。プリミティブは値で比較されるため、useCallback/useMemoで参照を固定する必要がありません。
Q5. Context.Providerのvalueは必ずuseMemoで包むべき?
A. 複合値(オブジェクト・配列)なら基本的にYes。ただし、購読側が少ない・購読側がそもそもmemo化されていない場合は効果が薄いです。Profilerで判断しましょう。
Q6. 依存配列に何を入れるべきか毎回悩みます。コツは?
A. ESLintのreact-hooks/exhaustive-depsに従うのが最短です。これに従ったうえで「依存が多すぎて使い物にならない」ときは、関数の中身を分割するか、ref(useRef)を使って参照だけ取り出すなど設計レベルで見直します。
Q7. Profilerが読みにくいです。何から見ればいい?
A. React DevToolsのProfilerタブを開き、「Ranked」表示で描画コストの大きい順にコンポーネントを並べます。上位のコンポーネントが「なぜレンダリングされたか(Why did this render?)」を確認すると、最適化の優先順位が一目でわかります。
まとめ:メモ化は「設計の最後の手段」
useMemoとuseCallbackは、Reactの再レンダリング制御における強力な道具ですが、決して「振りかけるだけで速くなる魔法」ではありません。本質は「同じ参照を保ち続けることでReact.memoの浅い比較を機能させる」こと。そのうえで、2026年現在はReact Compilerの普及により手動メモ化の出番は確実に減っています。まず計測、必要箇所だけ最適化、設計レベルで再レンダリング自体を減らす――この順序を徹底すれば、コードは綺麗なまま、アプリは確実に速くなります。Profiler・React DevTools・React Compiler・コンポジション設計を武器に、現場で評価されるReactエンジニアを目指していきましょう。

コメント