React Compilerを入れたらuseMemoとuseCallbackはいらなくなるのか、動かして確かめた
React Compilerを入れると、新しく書くコードでは useMemo と useCallback をほとんど書かなくてよくなります。ただし「完全に不要」ではありません。コンパイラが処理をスキップしたコンポーネントや、エフェクトの依存値を細かく制御したい場面では、今も手で書く意味が残ります。
筆者は検証環境で、TypeScript(.tsx)で書いた同じコンポーネントをコンパイラあり・なしで変換し、子の描画回数とエフェクトの実行回数を数えました。その結果と、公式ドキュメントの方針をまとめます。
検証環境は Ubuntu 24.04、Node.js 22.22.0 です。使ったのは react / react-dom 19.3.1、babel-plugin-react-compiler 1.0.0、@babel/core 8.0.7、@babel/preset-typescript 8.0.7、typescript 7.0.2、@types/react 19.3.0、jsdom 29.1.1 です。コード例は .tsx で書き、tsc で型チェックしてから、Babelで変換してjsdomの上で描画しました。ViteやNext.jsのビルドでは試していません。画像は、同じ変換結果を esbuild でまとめ、Playwright 1.56.1 の Chromium で動かして撮りました。
公式の答えは「基本は任せる、必要なところだけ書く」
公式ドキュメントの React Compilerの紹介ページ には、useMemo・useCallback・React.memo をどうすればよいかが書かれています。要点は次のとおりです。
- コンパイラは解析にもとづいて自動でメモ化する。多くの場合、手で書いたものと同じか、それ以上に細かい。
useMemoとuseCallbackは、メモ化を細かく制御したいときの「逃げ道(escape hatch)」として、コンパイラと一緒に使い続けられる。よくある例は、メモ化した値をエフェクトの依存値に使う場合。- 新しいコードは、コンパイラに任せ、必要なところだけ
useMemo/useCallbackを使う。 - 既存のコードは、今あるメモ化をそのまま残すか、外す前にしっかりテストする。外すとコンパイル結果が変わることがあるため。
同じ内容は React Compiler v1.0 の発表記事 にもあります。
useMemoなしの親で、子の再描画とエフェクトが止まるか
まず、useMemo も useCallback も書いていない親コンポーネントを用意しました。親はボタンを押すたびに自分の state を更新します。子は memo で包んであり、親から配列と関数を受け取ります。
export function Parent({ data }: { data: number[] }) {
const [count, setCount] = useState(0);
const items = data.filter((x) => x % 2 === 0); // 毎回新しい配列
const handleSelect = (i: number) => console.log('select', i); // 毎回新しい関数
const options = { size: items.length }; // 毎回新しいオブジェクト
useEffect(() => { log.effectRuns++; }, [options]);
return (
<div>
<button id="inc" onClick={() => setCount(count + 1)}>{count}</button>
<Child items={items} onSelect={handleSelect} />
</div>
);
}
コンパイラなしでは、配列と関数が毎回作り直されます。そのため memo があっても子は毎回描画され、エフェクトも毎回動くはずです。初回の描画のあと、ボタンを3回押した結果がこちらです。
$ node run.mjs ./out/app.plain.mjs
./out/app.plain.mjs: button=3 childRenders=4 effectRuns=4
$ node run.mjs ./out/app.compiled.mjs
./out/app.compiled.mjs: button=3 childRenders=1 effectRuns=1
コンパイラなし(plain)では、子の描画もエフェクトも4回です。コンパイラあり(compiled)では、どちらも初回の1回だけでした。useMemo も useCallback も書いていないのに、手で書いたときと同じ効果が出ています。
TypeScriptで書いても、Babelの設定はコンパイラのプラグインと @babel/preset-react に @babel/preset-typescript を足すだけでした。型注釈は変換の途中で取り除かれます。同じコンポーネントをJavaScript(.jsx)で書いた版でも試しましたが、この記事のどの計測も同じ回数になりました。
同じ2つの変換結果を、ブラウザでも並べて動かしました。esbuildで1つのファイルにまとめ、Playwright 1.56.1 の Chromium で上下のボタンを交互に3回ずつ押しました。1コマずつ撮って、0.08秒間隔でつないだGIFアニメーションです。画面上部の帯に何回目のクリックかが出て、押した直後に変わった数字はピンクになります。画面の下には、元のコードが同じ src/app.tsx であることを書き添えました。上(コンパイラなし)は押すたびに子の描画回数とエフェクトの実行回数が増え、下(コンパイラあり)は1のままです。

なお、検証で使った子コンポーネントは、描画回数を数えるためにモジュールの変数を書き換えています。これはReactのルール違反なので、子のほうはコンパイラにスキップされていました(検証ログでは line 8 のエラーとして記録)。子は memo で包んであるため、この比較には影響しません。
変換後のコードを見ると、理由が分かります。コンパイラは値ごとにキャッシュの枠($[0] など)を用意し、元になる値が変わったときだけ作り直します。
let t1;
if ($[0] !== data) {
t1 = data.filter(_temp);
$[0] = data;
$[1] = t1;
} else {
t1 = $[1];
}
const items = t1;
const handleSelect = _temp2;
handleSelect は外側の値を使っていないため、コンポーネントの外に出された関数(_temp2)になりました。毎回同じ関数が渡るので、子の memo が効きます。
すでに書いたuseMemoはどうなるか
次に、useMemo と useCallback をすでに書いたコンポーネントを変換しました。
type ListProps = { data: number[]; onPick: (i: number) => void };
export function List({ data, onPick }: ListProps) {
const items = useMemo(() => data.filter((x) => x % 2 === 0), [data]);
const handle = useCallback((i: number) => onPick(i), [onPick]);
return <ul>{items.map((i) => <li key={i} onClick={() => handle(i)}>{i}</li>)}</ul>;
}
変換後のコードで useMemo と useCallback を探すと、残っていたのは import の行だけでした。
$ grep -n 'useMemo\|useCallback' out/existing.compiled.mjs
2:import { useMemo, useCallback } from 'react';
呼び出しは、コンパイラ自身のキャッシュ(if ($[0] !== data) { ... } の形)に置き換わっていました。今回の例では、手で書いたメモ化がそのまま二重に残るわけではありません。ただ、公式は「外すとコンパイル結果が変わることがある」としています。既存のコードから消すかどうかは、上の方針どおりテストしてから決めるのが安全です。
コンパイラがスキップしたら、メモ化はゼロに戻る
コンパイラは Reactのルール に従ったコードを前提にしています。デバッグのページ によると、ルールを破っていそうなコードを見つけると、動きを変えてしまう危険を避けて、そのコンポーネントの最適化をスキップします。
そこで、さきほどの親に「レンダー中に props を書き換える」1行を足し、書き換えられるように props の型も変えました。これはルール違反ですが、型の上では書き換えてよいことになっているので、tsc の型チェックはエラーなしで通ります。
type Data = number[] & { lastRendered?: number };
export function Parent({ data }: { data: Data }) {
data.lastRendered = Date.now(); // レンダー中に props を書き換える(ルール違反)
// 以下は同じ
コンパイラのログには、次のエラーが出ました。ビルド自体は失敗せず、このコンポーネントは変換されずに元のまま出力されました。
CompileError line 16 | This value cannot be modified | Modifying component props or hook arguments is not allowed. Consider using a local variable instead
この状態で、useMemo なしの版と、useMemo / useCallback を手で書いた版を比べました。どちらもコンパイラを通しています。
$ node run.mjs ./out/bailout.compiled.mjs
./out/bailout.compiled.mjs: button=3 childRenders=4 effectRuns=4
$ node run.mjs ./out/bailout-manual.compiled.mjs
./out/bailout-manual.compiled.mjs: button=3 childRenders=1 effectRuns=1
コンパイラがスキップしたコンポーネントは、メモ化のないコードとして動きます。手書きの useMemo を消したあとにスキップされると、子の再描画やエフェクトの空振りが元に戻ります。
手書きの版は、コンパイラにスキップされても1回で止まりました。既存のメモ化を「残すか、テストしてから外す」という公式の方針は、こういう場面に効いてきます。
なお、関数の先頭に "use no memo" と書いても、コンパイラはその関数をスキップします。検証環境でも、付けた版は子の描画とエフェクトが4回ずつになりました。公式はこの指示を、デバッグのための一時的な手段としています。
ここまでの5つの版の計測結果を、1つのグラフにまとめました。どの版でも、エフェクトの実行回数は子の描画回数と同じでした。

今も手で書く意味がある場面
ここまでの結果と公式ドキュメントから、useMemo / useCallback を書く意味が残るのは次の場面です。
- エフェクトの依存値を、意図したときだけ変わるように細かく制御したいとき(公式が挙げている例)
- コンパイラがスキップしたコンポーネント(ルール違反がある、
"use no memo"が付いているなど) - 既存のコードで、外したときの影響をまだテストしていないとき
逆に、コンポーネントでもフックでもない、ふつうの関数はコンパイラの対象外です。検証環境でも、ただの関数は変換されずにそのまま出力されました。紹介ページ は、重い計算を複数のコンポーネントで使うなら、React の外で独自にメモ化することも考えるよう書いています。コンパイラのメモ化は、コンポーネントやフックの間で共有されないためです。
スキップされていないかを確かめる方法
どのコンポーネントがスキップされたかは、見た目では分かりません。筆者の検証では、Babelプラグインの logger オプション でイベントを受け取り、CompileSuccess と CompileError を出力して確かめました。
import type { Logger } from 'babel-plugin-react-compiler';
const logger: Logger = {
logEvent: (filename, event) => console.log(event.kind, 'fnName' in event ? event.fnName ?? '' : ''),
};
// Babel の設定: plugins: [['babel-plugin-react-compiler', { logger }]]
event はイベントの種類ごとに形が違う型です。検証環境で event.fnName と直接書くと、tsc がエラーにしました。そのため 'fnName' in event で確かめてから読んでいます。
v1.0 の発表記事 によると、ルール違反は eslint-plugin-react-hooks の推奨設定でも見つけられます。このリンターはコンパイラを入れていなくても使えるとされています。筆者はESLintでの確認は試していません。
新しいコードでは、まずコンパイラに任せて useMemo / useCallback を書かずに済ませられます。既存のメモ化は、コンパイラにスキップされたときの保険にもなるので、テストしてから外すかを決めます。

