React Compilerとは何か — 一言で言うと

React Compilerは、ビルド時にReactコンポーネントを解析し、必要なメモ化を自動で挿入するコンパイラです。 これまで開発者が手で書いていたuseMemouseCallbackReact.memoを、 「どこに、何を、どれだけ」入れるかをコンパイラが判断して埋め込みます。

Meta社内で長らく「React Forget」と呼ばれていたプロジェクトで、 2025年10月にv1.0が安定版としてリリースされ、Next.js 16では設定ひとつで有効化できる正式機能になりました。 Vite・Webpack・Rspackなど主要バンドラにもプラグインが提供されており、 2026年時点で「Reactを書くなら入れておく」が標準的な選択肢になっています。

なぜReact Compilerが必要だったのか

Reactの再レンダリングモデルは「状態が変わったらコンポーネント関数を再実行する」というシンプルなものです。 シンプルな代償として、子コンポーネントに渡すpropsや関数が毎回新しいインスタンスになるという現象が起きます。 これを抑え込むために、開発者はuseMemouseCallbackReact.memoを手で書いてきました。

手動メモ化の地獄

問題は、「いつメモ化すべきか」「どの依存配列が正しいか」を判断するのが想像以上に難しいことです。 依存配列を間違えれば古い値をキャプチャしてバグの温床になり、 過剰にメモ化すればキャッシュコストの方が高くついて逆に遅くなることもあります。

// 典型的な手動メモ化コード
function ProductList({ products, currency }: Props) {
  // この sort はメモ化すべき? 配列が毎回新しい参照になる
  const sorted = useMemo(
    () => [...products].sort((a, b) => a.price - b.price),
    [products]
  );

  // この onClick もメモ化すべき? 子が React.memo されているかで変わる
  const handleClick = useCallback(
    (id: string) => {
      analytics.track('click', { id, currency });
    },
    [currency]
  );

  return (
    <ul>
      {sorted.map(p => (
        <ProductRow
          key={p.id}
          product={p}
          onClick={handleClick}
        />
      ))}
    </ul>
  );
}

上のコード、一見正しく見えますが「Reactチームが本当に推奨する書き方」かは 子コンポーネント側の実装やデータサイズに依存します。 Reactの公式ドキュメントでも「useMemoはパフォーマンスのために使うべきで、まず計測せよ」と繰り返し書かれてきました。 しかし実務では「とりあえずメモ化しておけ」が氾濫し、コードベースは依存配列とコールバックでうるさくなる一方でした。

React Compilerが解決したい問題

React Compilerの設計目標は「メモ化のことを考えずにコンポーネントを書く」世界を作ることです。 開発者は素直に関数として書くだけでよく、どこに何のメモ化が必要かはコンパイラが判断する—— これがReact 19以降の新しいプログラミングモデルです。

どう動くのか — 自動メモ化の中身

React Compilerの仕事は、ざっくり以下の流れで進みます。

flowchart TB
    A[ソースコード .tsx] --> B[Babel/SWCで AST に変換]
    B --> C[React Compiler が中間表現 IR に変換]
    C --> D[コンポーネント関数を解析]
    D --> E{値ごとに依存関係を追跡}
    E --> F[再計算が必要な値だけ識別]
    F --> G[useMemo / useCallback 相当のキャッシュコードを挿入]
    G --> H[出力: 最適化済みの関数]

何が自動メモ化されるか

React Compilerは「コンポーネント関数の戻り値を構成する全ての中間値」を解析対象にします。 単にJSX全体をメモ化するのではなく、式単位の粒度でキャッシュを差し込みます。

// あなたが書くコード(メモ化なし)
function ProductList({ products, currency }: Props) {
  const sorted = [...products].sort((a, b) => a.price - b.price);

  const handleClick = (id: string) => {
    analytics.track('click', { id, currency });
  };

  return (
    <ul>
      {sorted.map(p => (
        <ProductRow key={p.id} product={p} onClick={handleClick} />
      ))}
    </ul>
  );
}

// React Compilerが内部的に生成するイメージ(疑似コード)
function ProductList({ products, currency }: Props) {
  const $ = useMemoCache(4);  // フックの内部用キャッシュスロット

  let sorted;
  if ($[0] !== products) {
    sorted = [...products].sort((a, b) => a.price - b.price);
    $[0] = products;
    $[1] = sorted;
  } else {
    sorted = $[1];
  }

  let handleClick;
  if ($[2] !== currency) {
    handleClick = (id) => analytics.track('click', { id, currency });
    $[2] = currency;
    $[3] = handleClick;
  } else {
    handleClick = $[3];
  }

  // 同じ要領で JSX もキャッシュされる
  return /* ... */;
}

ここで重要なのは、Compilerは「propsの何が変わったらこの値を再計算すべきか」を自動推論する点です。 人間が依存配列を書く必要はありません。間接的に参照しているクロージャ変数まで追跡してくれます。

導入方法 — フレームワーク別の設定

React CompilerはBabel/SWCのプラグインとして動きます。各環境での有効化方法を整理します。

Next.js 16以降

Next.js 16では設定ファイルでフラグを立てるだけで有効になります。React Compiler本体はNext.jsに同梱されているため別途インストールは不要です。

// next.config.ts
import type { NextConfig } from 'next';

const config: NextConfig = {
  reactCompiler: true,
  // または、より細かい制御:
  // reactCompiler: {
  //   compilationMode: 'annotation', // 'use memo'のついた関数だけ最適化
  // },
};

export default config;

Vite / Webpack / Rspack

Babelプラグインを直接設定します。Viteの場合は@vitejs/plugin-reactのBabelオプションに渡します。

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [
          ['babel-plugin-react-compiler', {
            target: '19', // Reactのターゲットバージョン
            // compilationMode: 'annotation',
          }],
        ],
      },
    }),
  ],
});

ESLintプラグイン(必須に近い)

React Compilerは「React Rules(純粋性ルール)」を守ったコードでのみ正しく動きます。 違反箇所はCompilerがbail outして最適化対象から外すため、 違反を可視化するESLintプラグインの導入は実質必須です。 React 19.2以降ではeslint-plugin-react-hooksのrecommended-latestにCompiler用ルールが統合されました。

// .eslintrc.js または eslint.config.js
{
  "extends": [
    "plugin:react-hooks/recommended-latest"
  ]
}

// もしくは個別に
{
  "plugins": ["react-compiler"],
  "rules": {
    "react-compiler/react-compiler": "error"
  }
}

'use memo' / 'use no memo' — 粒度を制御する

Compilerには3つのcompilationModeがあり、最適化の適用範囲を選べます。

モード 挙動 用途
infer(デフォルト) コンパイラが「Reactコンポーネント・カスタムフックっぽいもの」を自動判定して最適化 ほぼ全プロジェクトで推奨。命名規約(大文字始まり、useプレフィックス)に従っていれば自動的に対象になる
annotation 'use memo' ディレクティブが付いた関数だけ最適化 既存巨大コードベースへの段階的導入。リスクを限定したい初期フェーズ向け
all すべての関数を最適化対象に 実験用途。通常のReactコードベースでは過剰でほぼ使われない

'use memo' で個別オプトイン

annotationモードでは、関数本体の先頭に'use memo'ディレクティブを置いた関数だけがCompilerの対象になります。 「まずは1ファイルだけ試したい」というときに便利です。

function ExpensiveComponent({ items }: Props) {
  'use memo'; // ← この関数だけCompilerが最適化する

  const sorted = items.sort((a, b) => b.score - a.score);
  return <List items={sorted} />;
}

'use no memo' で個別オプトアウト

逆に「inferモードで全体に効かせつつ、この関数だけは触ってほしくない」場合は'use no memo'を使います。 Compilerが誤判定したり、後述のbail outが連鎖して問題を起こす場合の緊急エスケープハッチです。

function LegacyComponent(props: Props) {
  'use no memo'; // ← この関数はCompilerが触らない

  // mutation を多用する古いコードなど
  let state = {};
  /* ... */
  return <div />;
}

React Rulesとbail out — 動かない条件

Compilerは「Reactのルールを守ったコード」でのみ正しく最適化できます。 ルール違反を検出するとそのコンポーネント・フックを丸ごと最適化対象から外す(bail out)挙動を取ります。 クラッシュさせるのではなく「黙って最適化を諦める」のがポイントで、これがCompilerが既存コードでも壊さずに導入できる理由です。

守るべきReact Rules

  • コンポーネントとフックは純粋関数であること。レンダー中にprops・stateを直接ミューテーションしない
  • Rules of Hooksを守る。条件分岐の中でフックを呼ばない、トップレベルのみ
  • レンダー中に副作用を起こさないconsole.logを除き、APIコール・DOM操作・refの書き換えなどはuseEffectやイベントハンドラへ
  • refやコンテキスト値をレンダー中に読み書きしない

bail outしてしまうコード例

// NG: propsを直接ミューテーションしている
function BadList({ items }: Props) {
  items.sort((a, b) => a.id - b.id); // ← items を破壊的に変更
  return <List items={items} />;
}

// OK: イミュータブルに扱う
function GoodList({ items }: Props) {
  const sorted = [...items].sort((a, b) => a.id - b.id);
  return <List items={sorted} />;
}

// NG: レンダー中に ref を読み書きしている
function BadCounter() {
  const ref = useRef(0);
  ref.current += 1; // ← レンダー中のミューテーション
  return <p>{ref.current}</p>;
}

// OK: useEffectに逃がす
function GoodCounter() {
  const ref = useRef(0);
  useEffect(() => {
    ref.current += 1;
  });
  return <p>{ref.current}</p>;
}

既存のuseMemo/useCallbackをどう扱うか

最大の関心事は「既存の手書きメモ化は消していいのか」だと思います。結論から言うと:

既存のメモ化はCompilerが尊重する

React Compilerは既存のuseMemouseCallbackReact.memoを勝手に書き換えたり削除したりしません。 したがって導入時点で何も壊れません。これがBreaking Changeを避ける設計の核です。

ただし、Compilerが既にメモ化を入れる箇所に手書きのメモ化が残っていると、同じ計算を二重にキャッシュすることになり、 わずかにオーバーヘッドが増えます。理想的にはCompiler導入後に不要な手書きメモを段階的に削除していきます。

推奨の移行フロー

  1. ビルドツール側でCompilerを有効化(inferモード推奨)
  2. ESLintプラグインを導入してReact Rules違反を一掃
  3. 動作確認 + パフォーマンス計測(Lighthouse、React DevTools Profiler)
  4. 不要になった手書きメモを段階的に削除eslint-plugin-react-compilerに「冗長メモ化検出」のルールがあるのでそれを目印に
  5. 新規コードはメモ化なしで書く運用に切り替え

よくある誤解

誤解1: 入れれば自動で全部速くなる

Compilerは「無駄な再レンダリングを抑える」だけで、計算量そのものを減らすわけではありません。 もともとProfilerで赤くなっていなかったコンポーネントには体感差はほぼ出ません。 ボトルネックがネットワークやレンダリングの計算量にあるアプリでは、Compilerだけでは解決しません。

誤解2: useMemoを全部消していい

Compilerが対象とするのは「レンダー中の値の再計算抑制」です。 useMemoを「参照の安定性を保ちたい」目的で使っているケース(依存配列の同一性に依存する外部ライブラリへの引数など)は、 Compilerに任せきれない場合があります。コード意図を読まずに機械的に削除しないようにしましょう。

誤解3: Compilerが入ったらReact.memoは不要

Compilerは親コンポーネントの再レンダリング自体は止めません。 あくまで「propsが変わらなければ子のpropsとして渡す値の参照が安定する」ことを保証します。 子コンポーネント自体の再実行を止めるには、依然としてReact.memoでラップする必要があります(ただし手書きのpropsメモ化はもう要りません)。

誤解4: 開発環境でも本番と同じ高速化が得られる

Compilerはビルド時に動くため、開発モード(HMR)でも有効です。ただし、Strict Modeの二重レンダリング各種ランタイムチェックが入るぶん、開発時の体感は本番ほど速くなりません。 パフォーマンス計測は必ず本番ビルドで行ってください。

まとめ — 入れない理由がほぼ無い

React Compilerは「Reactの書き方を変える」というより、「これまで手で書いていた最適化をコンパイラが肩代わりする」ツールです。 既存コードを壊さず、ESLintと組み合わせれば導入リスクは限りなく低く、新規プロジェクトはもちろん、 既存プロジェクトでも入れない理由を見つけるほうが難しい段階に来ています。

一方で「銀の弾丸ではない」ことも忘れてはいけません。 React Rules違反のコードはbail outで素通しになり、最適化の恩恵を受けません。 Compilerがあるからこそ、「Reactらしい純粋な書き方」を守ることのリターンが大きくなったとも言えます。 メモ化から解放されたぶんの集中力を、コンポーネントの責務分割や状態設計に回せるようになる—— それが2026年のReact開発のスタンダードな景色です。

理解度チェック

問題 0 / 50%
Q1

React Compilerでコンポーネント単位に最適化をオプトインしたいとき、関数本体の先頭に書くディレクティブは「___」である。