React Compilerは、Reactコンポーネントとフックをビルド時に解析し、useMemo・useCallback・React.memoに相当するメモ化を自動で挿入する公式ツールです。2025年10月7日に初の安定版1.0が出て、2026年10月31日時点でもnpmのbabel-plugin-react-compilerの最新版は1.0.0のままです。
一方で、周辺のビルドツールはこの1年で大きく変わりました。ReactリポジトリへのRust移植のマージ(2026年6月)、Oxc・Rspackへの組み込み、Next.js 16での正式な設定項目化、Vite 8でのBabel廃止です。本記事は、導入手順をビルドツール別に示し、コンパイルされないコードとESLintでの検出範囲を検証環境で確かめた結果とあわせて整理します。
まとめ:React Compilerの要点
- 何をするか:ビルド時にコンポーネントへキャッシュ用のコードを挿入し、入力が変わらない値・JSX・関数を再利用します。手書きの
useMemoは原則不要になります。 - 版:
babel-plugin-react-compiler1.0.0(2025-10-07)が最新の安定版です。React 17・18でもtarget指定で使えます。 - Vite 8:
@rolldown/plugin-babel経由のBabel版と、react({ compiler: true })のRust版(実験的)の2通りがあります。 - Next.js 16:
reactCompiler: trueは安定版の設定になりましたが、既定では無効です。 - Expo:SDK 54以降の新規テンプレートで既定有効です。
- ESLint:
eslint-plugin-react-compilerは不要です。eslint-plugin-react-hooks7系のrecommendedにCompiler由来のルールが入っています。 - 落とし穴:Compilerがルール違反などを検出したコンポーネントは、既定設定ではビルドを止めずに対象外になります。ただし、すべての違反を検出できるわけではありません。Babel版を
@babel/core8と組み合わせると、既定値付きpropsのコンポーネントも黙って対象外になりました。
React Compilerの仕組み:ビルド時の自動メモ化
React Compilerは実行時のライブラリではなく、ソースコードを書き換える変換器です。Reactのルール(コンポーネントは純粋、propsとstateは変更しない、フックは毎回同じ順で呼ぶ)を前提に、どの値がどの入力に依存するかをデータフローで追い、入力が同じなら前回の結果を返すコードを生成します。
開発の起点は2017年のPrepackで、2021年に「React Forget」として最初の版が披露されました。React公式の1.0発表は、Meta Quest Storeで初回読み込みとページ遷移が最大12%改善し、一部の操作は2.5倍以上速くなり、メモリ使用量は変わらなかったと報告しています。
変換後のコード:_c()のキャッシュ配列と前回値の比較
次のコンポーネントをbabel-plugin-react-compiler 1.0.0で変換しました。
export function TodoList({ todos, filter }) {
const visible = todos.filter((t) => t.status === filter);
return <ul>{visible.map((t) => <li key={t.id}>{t.text}</li>)}</ul>;
}
出力(抜粋)は次のとおりです。_c(7)で7要素のキャッシュ配列を確保し、todosとfilterが前回と同じなら、絞り込み結果も<ul>要素も再計算しません。
import { c as _c } from "react/compiler-runtime";
export function TodoList(t0) {
const $ = _c(7);
const { todos, filter } = t0;
let t1;
if ($[0] !== filter || $[1] !== todos) {
// filter 用のコールバックも $[3]・$[4] にキャッシュ
const visible = todos.filter(t2);
t1 = visible.map(_temp);
$[0] = filter;
$[1] = todos;
$[2] = t1;
} else {
t1 = $[2];
}
// <ul> 要素も t1 が同じなら $[6] を返す
...
}
手書きのuseMemoは依存配列ごとに1つのキャッシュですが、Compilerは式単位で細かく分けます。.map()のコールバックは外部の値を使わないため、_tempという関数としてコンポーネントの外へ移されています。
自動メモ化で減る再レンダーの実測
親コンポーネントが毎回オブジェクトリテラルとアロー関数を子に渡し、子のuseEffectがそのオブジェクトに依存する構成を、jsdom上でReact 19.3.1を使って描画しました。親のstateをクリックで5回更新したときの回数です(React.memo・useMemoは一切書いていません)。
| 計測項目 | Compilerなし | Compiler 1.0.0 |
|---|---|---|
| 子コンポーネントの描画 | 6回 | 1回 |
| 子のuseEffect実行 | 6回 | 1回 |
| 親の重い計算 slowSum(n) | 6回 | 1回 |
初回の1回を含み、StrictModeは使用していません。更新したのは計算入力とは無関係なstateで、slowSumに渡すnは固定しています。子を表すJSX要素がキャッシュされるため、React.memoで包まなくても子の再実行が止まりました。useEffectの依存配列にオブジェクトを入れて毎回発火する、というよくある不具合もこの構成では起きなくなります。
コンパイルの対象になる関数とならない関数
既定のcompilationMode: 'infer'は、名前とJSX・フックの使用から対象を推定します。大文字で始まりJSXを返す関数はコンポーネント、useで始まりフックを呼ぶ関数はフックとして扱われます。useで始まってもフックを1つも呼ばない関数や、小文字の通常関数は変換されませんでした。クラスコンポーネントも対象外です。
| compilationMode | 対象 |
|---|---|
'infer'(既定) |
命名規則とJSX・フックの使用から推定 |
'annotation' |
"use memo"を書いた関数だけ |
'syntax' |
Flowのcomponent・hook構文だけ |
'all' |
トップレベルの全関数(非推奨) |
4つの値と説明はcompilationModeのリファレンスにあります。'syntax'はFlow専用でTypeScriptでは動きません。
React Compilerを導入する手順(2026年10月時点)
新規アプリなら、npm create vite@latestとnpx create-next-app@latestでCompiler有効のテンプレートを選べます。既存アプリへの導入はビルドツールごとに書き方が違います。
Vite 8で導入する:Babel版とRust版の2通り
@vitejs/plugin-react 6系はBabelを内蔵しなくなったため、Babel版のCompilerは@rolldown/plugin-babelを別に入れて使います。plugin-reactのREADMEの構成は次のとおりです。
npm install -D @rolldown/plugin-babel @babel/core@7 babel-plugin-react-compiler
// vite.config.js
import { defineConfig } from 'vite'
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
import babel from '@rolldown/plugin-babel'
export default defineConfig({
plugins: [react(), babel({ presets: [reactCompilerPreset()] })],
})
READMEのインストール例は@babel/coreを版指定なしで書いていますが、上では@7を付けています。理由は次の章で説明します。
6.1.0(2026年8月19日)からは、Rust移植版を使うcompilerオプションが加わりました。READMEは「Native React Compiler support is experimental.」と明記しています。
npm install -D oxc-transform-react
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react({ compiler: true })],
})
6.1.2のpeer依存はoxc-transform-react ^0.152.0です。npmの最新は0.156.0(2026-10-27)ですが、版を付けずにinstallすると範囲内の0.152.0が入りました。react({ compiler: { compilationMode: 'annotation' } })のようにCompilerのオプションも渡せます。プラグインそのものの設定は@vitejs/plugin-react とは?v6.1の導入とvite.config設定・React Compiler・SWC版との違い、Vite 8の環境構築はVite + Reactの環境構築手順で扱っています。
Next.js 16で導入する:reactCompilerは安定版の設定
Next.js 16でreactCompilerはexperimentalから外れて安定版の設定になりました。Next.js 16の発表は「It is not enabled by default」としており、自分で有効にする必要があります。
npm install -D babel-plugin-react-compiler
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
reactCompiler: true,
}
export default nextConfig
Next.jsのreactCompilerドキュメントによると、Compiler本体はBabelプラグインとして動き、Next.jsはSWCで事前にファイルを解析して、JSXやフックを含むファイルにだけBabelをかけます。全ファイルにBabelを通すより速くなりますが、Compilerを使わない場合のビルドよりはわずかに遅くなると書かれています。Next.js 15でexperimental.reactCompilerと書いていた設定は、16ではトップレベルへ移します。
Expo(React Native)で導入する:SDK 54以降は既定で有効
React CompilerはReact Nativeでも動きます。Expoのガイドによると、SDK 54以降はnpx create-expo-app@latestの標準テンプレートでCompilerが有効になっており、SDK 55以降はeslint-config-expoにCompilerのlintルールも含まれます。SDK 54以降はBabelの設定も自動で入るため、既存プロジェクトではapp.jsonの"experiments": { "reactCompiler": true }を足すのが有効化の手順です。SDKごとの変更点はExpo SDK 58・57・56の変更点とアップグレード手順にまとめています。Compiler有効の構成でiOS・Androidアプリを開発する場合は、Flutter / React Native開発の支援も行っています。
React 17・18で使う:targetとreact-compiler-runtime
React 19より前の版では、targetのリファレンスどおり、react-compiler-runtimeを実行時の依存として入れ、targetを文字列で指定します。
npm install react-compiler-runtime@latest
// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', { target: '18' }], // React 17 なら '17'
],
};
変換後のimport元は、既定のreact/compiler-runtimeからreact-compiler-runtimeに変わりました。React本体の版の確認と19への更新手順はReactの最新バージョンは19.3|React 19の新機能・18との違いと確認・更新方法を参照してください。
Babel 8併用時の既定値付きpropsの変換漏れ
Vite 8のREADMEどおりにnpm install -D @rolldown/plugin-babel @babel/core babel-plugin-react-compilerと版を付けずに入れると、2026年10月31日時点では@babel/core 8.0.10が入ります。この状態で次の2つをビルドしました。
export function Size({ size = 1 }) { return <p>{[size].length}</p>; }
export function Plain({ size }) { return <p>{[size].length}</p>; }
| 変換方法 | Plain(既定値なし) | Size(既定値あり) |
|---|---|---|
| Babel版 + @babel/core 8.0.10 | 変換される | 変換されない |
| Babel版 + @babel/core 7.29.7 | 変換される | 変換される |
| Rust版 oxc-transform-react 0.152.0 | 変換される | 変換される |
Babel 8ではSizeが元のコードのまま出力され、警告もビルドエラーも出ませんでした。function Button({ variant = 'primary' })のような書き方はよく使われるため、Babel 8環境では対象外のコンポーネントが気づかないまま増えます。
Babel版を使うなら@babel/core@7に固定するか、Rust版のcompiler: trueに切り替えてください。どちらの場合も、ビルド後に下の「コンパイル結果の確認」の手順で、主要なコンポーネントがMemo ✨ 表示になっているかを一度確認しておくと安全です。
Rust版React Compiler:SWC・Oxc・Rspackの対応状況
1.0の時点ではCompilerの実装はBabelプラグインだけで、SWCやOxcで動くビルドツールでもCompilerの部分はBabelを通していました。2026年に入って、Rustで書かれたCompilerを各ツールが直接使えるようになっています。
| ツール | 使い方 | 状態 |
|---|---|---|
| React CompilerのRust移植 | PR #36173 | 2026-06-09マージ・npm未公開 |
| Rspack 2.1 | jsc.transform.reactCompiler | 2026-06-26公開 |
| Oxc(oxc-transform-react) | Vite plugin-react 6.1.0〜 | 実験的・2026-08-18発表 |
| Next.js 16 | SWCで対象を絞りBabelで変換 | 安定版の設定 |
PR #36173「Port React Compiler to Rust」は2026年6月9日にReactリポジトリへマージされました。リポジトリ内にはbabel-plugin-react-compiler-rustというパッケージ定義(版は0.0.0-experimental)がありますが、npmには公開されていません(2026年10月31日に照会して404)。
Rspack 2.1の発表は、builtin:swc-loaderのjsc.transform.reactCompiler: trueでRust版を有効にでき、Babel版よりrspack devが13.5倍、rspack buildが7.4倍速かったとしています。
Oxcの発表は、Rust移植は未完成でBabel版との不一致もあったため、CompilerをOxcに取り込んでOxcのASTで直接動かすようにした、と経緯を説明しています。予備的なベンチマークでBabelの10倍以上速いとしつつ、取り込んだコードにはまだTODOが残ると書いています。
Vite 8で400個のコンポーネントを生成してビルド時間を測ると、Compilerなしが約0.9秒、Rust版が約0.95秒、Babel版(@babel/core 8)が約5.6秒でした(Intel Core i9-9880H、Node.js 26.5.0、npxの起動を含む実時間を各3回計測。minifyは無効で、Rust版・Babel版とも400個すべてを変換)。ビルド時間が問題になる規模なら、まずRust版を試す価値があります。ただし実験的扱いなので、本番ビルドに使う前にBabel版と出力の差がないかを主要画面で確かめてください。SWCとOxcの違いはSWCとは?Rust製コンパイラの設定と採用判断を実装目線で解説とOxcとは?Rust製JavaScriptツールチェーンの構成と使い方・SWCとの違いで整理しています。
コンパイルされないコードと確認方法
CompilerはReactのルールに反しそうなコードを見つけると、そのコンポーネントを変換せずに元のまま残します。panicThresholdの既定値'none'では、この対象外判定でビルドは止まりません。
対象外になる書き方の実測
babel-plugin-react-compiler 1.0.0とeslint-plugin-react-hooks 7.1.1のrecommendedに、同じ検証コードを通した結果です。表は今回のコードに対する結果であり、同じ構文を使うすべてのコードの変換可否や診断を保証するものではありません。
| 書き方 | Compiler | ESLint |
|---|---|---|
| render中にref.currentを読み書き | 対象外 | error(refs) |
| if の中でuseState | 対象外 | error(rules-of-hooks) |
| render中に無条件でsetState | 対象外 | error |
| モジュール変数を counter++ | 対象外 | 検出なし |
| react-hook-formのwatch() | 対象外 | warning |
| props の配列に push | 変換される | 検出なし |
| try/catch | 変換される | 検出なし |
注意したいのは下の2つです。モジュール変数の変更はCompilerが対象外にするのにESLintは何も出さないため、lintが通っても最適化されていないコンポーネントが残ります。propsの配列へのpushはどちらも検出せず、Compilerは変換しました。propsを変更しないというルールを守るのは、引き続き書き手の責任です。何がルール違反になるかはReactの純粋関数とは?コンポーネントを純粋に保つルールとESLintの検出範囲で詳しく扱っています。
react-hook-formのwatchからuseWatchへの置換
incompatible-libraryルールは、オブジェクトの参照を変えずに中身だけを変える「interior mutability」を持つAPIを既知のリストで検出し、そのコンポーネントをCompilerの対象から外します。react-hook-formのwatchとTanStack TableのuseReactTableが例に挙がっています。
// 対象外:Use of incompatible library
const { register, watch } = useForm();
const name = watch('name');
// 変換される:useWatch に置き換え
const { register, control } = useForm();
const name = useWatch({ control, name: 'name' });
実際に上の2つを変換すると、watch版は対象外、useWatch版は_c(11)で変換されました。MobXのobserverも同じ理由でメモ化と相性が悪いものの、公式ドキュメントは「linterはまだ検出しない」としています。MobXを使うコンポーネントで表示が更新されなくなったら、関数の先頭に"use no memo"を書いて対象から外します。
コンパイル結果の確認:DevToolsと診断設定
- React DevTools:変換されたコンポーネントには「Memo ✨」のバッジが付きます。主要画面で付いていないものが対象外です。
- panicThreshold:CIで一時的に
'all_errors'にすると、コンパイラ診断が発生した際にビルドが失敗します。ただし、ディレクティブや対象選択の設定で除外された関数をすべて検出する機能ではありません。検証では「Found 3 errors」と3件の理由が出ました。通常のビルドは既定の'none'に戻します。 "use no memo":Compiler導入後に挙動が変わったコンポーネントを一時的に外す退避策です。原因を直したら外します。
ESLint:旧Compilerプラグインからreact-hooks 7系への移行と検出範囲
単体のeslint-plugin-react-compilerはnpmの最新が19.1.0-rc.2(2025-05-14)のまま更新されていません。1.0の発表は、このプラグインを外してeslint-plugin-react-hooks@latestへ移るよう案内しています。eslint-plugin-react-hooksは7.0.0からrecommendedでCompiler由来のルールが既定で有効になりました。7.1.1(2026-04-17)のflat configでは次のように書きます。
// eslint.config.mjs
import reactHooks from 'eslint-plugin-react-hooks';
export default [
{
files: ['src/**/*.{js,jsx}'], // JavaScript・JSX用。TypeScriptには別途パーサー設定が必要
languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
},
reactHooks.configs.flat.recommended,
];
7.1.1のconfigs.flat.recommendedには16ルールが入っています。従来のrules-of-hooksとexhaustive-depsに加え、Compiler由来のrefs・immutability・purity・globals・set-state-in-render・set-state-in-effect・static-components・preserve-manual-memoization・incompatible-libraryなどです。recommended-latestはvoid-use-memoを足した17ルールでした。プリセットはJSXのパーサー設定を含まないため、上のecmaFeatures.jsxを書かないとJSXが構文エラーになります。
Oxlintも、Compilerの検証処理を使うルールを22個持っています(Oxcの発表による)。ESLintからの移行を検討している場合はOxlintとESLintの違いとは?読み方・速度実測・移行手順とBiomeとの比較を参照してください。
useMemo・useCallbackの削除条件と維持すべき場面
公式の導入ページの方針は、新しいコードではCompilerに任せて必要な箇所だけuseMemo・useCallbackを使う、既存コードの手書きメモ化は残すか、十分にテストしてから消す、です。消すとコンパイル結果が変わる場合があるためです。
- 新規コード:原則書かない。メモ化はCompilerが解析結果に基づいて行います。Reactカスタムフックのベストプラクティスの検証例では、
useMemoを書いた版と書かない版の変換結果が一致しました(すべてのコードで一致する保証ではありません)。 - 残す:
useEffectの依存配列に入る値で、不要な再実行を減らしたい場合。ただし、メモ化はキャッシュの保持やEffectの実行回数を保証しないため、処理の正しさをメモ化に依存させないでください。公式もこの用途を「escape hatch」として挙げています。上の表で対象外になる書き方を含むコンポーネントでは、手書きのメモ化だけが効いています。 - 残す:未コンパイルのまま配布し、利用側でもCompilerが動く保証のない共有コンポーネントやライブラリ。配布前にコンパイル済みなら、利用側にCompilerがなくても生成されたメモ化は働きます。
- 消してよい:変換済み(Memo ✨)で、値を画面に出すだけの
useMemoや、子へ渡すだけのuseCallback。消す前後でテストを通します。
手書きのメモ化は、依存配列に漏れがあるとコンポーネントごと対象外になります。useMemo(() => items.filter((i) => i === q), [items])のようにqを書き漏らすと、Compilerは「Existing memoization could not be preserved」で変換をやめました。このときESLintが出したのはexhaustive-depsのwarning 1件だけです。warningを放置したままのコードベースでは、Compilerが効いていないコンポーネントが残ります。フック全体の使い分けはReactフック(Hooks)とは?全18種の一覧とReact Compiler時代の使い分けにまとめています。
既存プロジェクトへの段階導入と、導入を見送る場面
既存のアプリにいきなり全体適用すると、対象外のコンポーネントと挙動が変わったコンポーネントを切り分けにくくなります。次の順で範囲を広げます。
- ESLintを
eslint-plugin-react-hooks7系に上げ、errorを先に直す。 compilationMode: 'annotation'にして、効果を見たいコンポーネントにだけ"use memo"を書く。- ディレクトリ単位に広げる。Babelでは
overrides、ViteではreactCompilerPreset()のrolldown.filter.id.excludeで対象外のディレクトリを指定できます。 - 全体を
'infer'にし、DevToolsとテストで確認する。
次の場合は、Compilerを入れる前に別の対処を優先してください。
- MobXの
observerが中心のアプリ:lintで検出できず、更新が止まる箇所を目視で探すことになります。対象を'annotation'で限定する運用が現実的です。 - クラスコンポーネントが大半のアプリ:変換対象は関数コンポーネントとフックだけなので、効果が出ません。
- 遅さの原因が描画回数でないアプリ:通信待ちや巨大なバンドル、多数のDOM要素の初回描画が主因なら、それぞれ通信・配信・リストの仮想化への対処を優先します。ただし、長いリストでも不要な再レンダーや再計算にはCompilerが効く場合があります。DevToolsのProfilerで再レンダーが原因かを先に確かめます。
よくある質問
React CompilerはSWCでも使えますか?
SWC単体向けの公式プラグインはまだありません。Next.jsはSWCで対象ファイルを絞り、Compiler本体はBabelプラグインで動かします。RspackはSWCベースのbuiltin:swc-loaderにjsc.transform.reactCompilerがあり、2.1からRust版を使えます。
eslint-plugin-react-compilerはまだ必要ですか?
不要です。eslint-plugin-react-hooks 7系のrecommendedにCompiler由来のルールが含まれています。eslint-plugin-react-compilerは19.1.0-rc.2から更新されていないため、アンインストールして構いません。
React CompilerでuseMemoやuseCallbackは完全に不要になりますか?
新規コードではほぼ不要です。ただし、effectの依存配列に入れて不要な再実行を減らしたい値と、Compilerの対象外になったコンポーネントでは、手書きのメモ化が引き続き必要です。既存のuseMemoは、テストを通してから消してください。
React CompilerはReact Nativeでも使えますか?
使えます。Expo SDK 54以降の新規テンプレートでは既定で有効です。既存のプロジェクトではapp.jsonのexperiments.reactCompilerをtrueにします。
React 18でもReact Compilerは使えますか?
使えます。react-compiler-runtimeをインストールし、Compilerの設定でtarget: '18'を指定します。React 17も'17'で対応しています。