第4回
React Compiler 1.0で手書きメモ化を卒業する
安定版になったReact Compilerを実際に試す。Vite 8環境への導入からDevToolsでの効果確認、既存のuseMemoとの付き合い方までを動かしながら解説する。
「React最新ラボ」第4回。今回はReactチーム公式の React Compiler 1.0 を、このサイトと同じ Vite 環境に入れて効果を確かめる。
React Compilerは何をするのか
React Compilerはビルド時にコンポーネントのコードを解析して、自動でメモ化コードに変換するBabelプラグインだ。実行時のライブラリではなく、コンパイル段階の変換なので、書くコードはプレーンなままでいい。
| 手書きメモ化(これまで) | React Compiler | |
|---|---|---|
| 書くもの | useMemo / useCallback / React.memo を自分で配置 |
何も書かない(プレーンなコード) |
| 抜け漏れ | 人間の判断次第。入れ忘れ・入れすぎが起きる | コンパイラが一律に解析 |
| 依存配列 | 自分で管理。ミスるとバグ | 不要 |
| 対応バージョン | ─ | React 19推奨(17/18はランタイムパッケージ追加で可) |
導入する(Vite 8 + plugin-react v6 構成)
このサイトと同じ Vite 8 + @vitejs/plugin-react v6 の構成で入れてみる。v6からプラグイン内部のBabelが廃止されてRust製の変換(oxc)になったため、Babelプラグインである React Compiler は @rolldown/plugin-babel を別途足して動かす形になる。
npm i -D @rolldown/plugin-babel babel-plugin-react-compiler// vite.config.ts
import { defineConfig } from 'vite'
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
import { babel } from '@rolldown/plugin-babel'
export default defineConfig({
plugins: [
// ★ babel() を react() より先に置く(コンパイラを先に通すため)
babel({
include: /\.[jt]sx?$/,
babelConfig: reactCompilerPreset(),
}),
react(),
],
})reactCompilerPreset() は @vitejs/plugin-react が用意している設定済みプリセットで、babel-plugin-react-compiler を適切なデフォルトで組み込んでくれる。
Vite 7以前(plugin-react v5以前)なら従来どおり
react({ babel: { plugins: ['babel-plugin-react-compiler'] } })の1行で有効化できる。
効果を確かめる
わざと「親が更新されると子が巻き添えで再レンダリングされる」構成を作る。
// ./src/App.tsx
import { useState } from 'react'
import { HeavyList } from './HeavyList'
export function App() {
const [count, setCount] = useState(0)
return (
<main>
<button onClick={() => setCount((c) => c + 1)}>カウント: {count}</button>
{/* count と無関係なのに、毎回再レンダリングされていた子 */}
<HeavyList items={['React', 'Vite', 'Tailwind']} />
</main>
)
}// ./src/HeavyList.tsx
export function HeavyList({ items }: { items: string[] }) {
console.log('HeavyList rendered')
return (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)
}コンパイラなしだと、ボタンを押すたびに HeavyList rendered がコンソールに流れる。コンパイラ有効化後は初回の1回だけになる。React.memo も useMemo も書いていないのに、だ。
コンパイラなし
App:11回
HeavyList:11回(毎回巻き添え)
コンパイラあり
App:11回
HeavyList:1回(propsが同じなのでスキップ)
もう一つの確認方法が React DevTools。Componentsタブでコンパイル済みコンポーネントには 「Memo ✨」バッジが付く。バッジが付いていなければ、そのコンポーネントはコンパイラの対象から外れている。
コンパイラが効かないとき:Rules of React
だから導入とセットで、リンタも最新にしておく。eslint-plugin-react-hooks の最新版にはコンパイラと同じ解析エンジンによるルールが入っていて、「なぜこのコンポーネントが最適化されないのか」を教えてくれる。
npm i -D eslint-plugin-react-hooks@latestまた、特定のコンポーネントだけコンパイラから除外したいときは、関数の先頭に "use no memo" ディレクティブを書く逃げ道もある。
function ProblemChild() {
'use no memo' // このコンポーネントはコンパイラの対象外にする
// ...
}既存の useMemo / useCallback は消すべき?
すぐに全部消す必要はない。コンパイラは手書きのメモ化と共存できるので、推奨の進め方はこうなる。
- まずコンパイラを有効化する(既存コードはそのまま)
- DevToolsとリンタで、最適化が効いているのを確認する
- 新しいコードからメモ化を手書きしない習慣に切り替える
- 既存のラップは、触るついでに少しずつ剥がす
つまずきやすい所
- 効果が見えない:もともと再レンダリングが問題になっていないアプリでは体感差は出ない。DevToolsの「Memo ✨」バッジとコンソールのレンダリング回数で確認する
- 一部だけバッジが付かない:Rules of React違反が濃厚。リンタの警告を確認する
- plugin-react v6でbabelオプションが効かない:v6でBabel内蔵が廃止されたため。この記事の
@rolldown/plugin-babel構成に切り替える
まとめ
- React Compiler 1.0はビルド時の自動メモ化。
useMemo/useCallback/React.memoを手書きしなくてよくなる - Vite 8 + plugin-react v6では
@rolldown/plugin-babel+reactCompilerPreset()で有効化する - 効果はDevToolsの「Memo ✨」バッジとレンダリング回数で確認する
- 前提はRules of React。違反コードは黙ってスキップされるので、
eslint-plugin-react-hooks最新版を併用する
次回は、React 19.2で安定版になった <Activity> と useEffectEvent を試す。「タブを切り替えても入力が消えないUI」と「useEffectの依存配列の悩み」を一気に解決する2つだ。