React Compiler 徹底入門 - useMemo / useCallback から解放される新時代の最適化

2026.04.09

公式ドキュメント

この記事の要点

• useMemo/useCallback/React.memoの手動最適化をビルド時に自動化するコンパイラ
• Babelプラグインとして動作し、Rules of Reactに従っていれば自動で最適化
• ESLintプラグインで事前にコンパイラ互換性を確認可能

React Compiler は、React チームが公式に開発している自動メモ化コンパイラです。これまで手動で useMemo / useCallback / React.memo を貼り付けて行っていた再レンダリング最適化を、ビルド時に自動化してくれる仕組みです。本記事では、React Compiler の動作原理、導入手順、書き方の変化、注意点、既存プロジェクトを移行するときのベストプラクティスを整理します。

概要

flowchart LR
    Source["JSX / Hooks コード"] --> Compiler["React Compiler (Babel plugin)"]
    Compiler --> Output["メモ化済み JS"]
    Output --> Bundler["Vite / Next.js / webpack"]
    Bundler --> Browser["ブラウザ実行"]
    Compiler -. lint .-> ESLint["eslint-plugin-react-compiler"]

React Compiler は Babel プラグインとして動作し、React コンポーネントとフックを解析、必要に応じてメモ化コードを自動挿入します。開発者は素直に書くだけで、React の Rules of React に従っている限り適切な最適化が得られる、という設計思想です。

主な特徴

1. 自動メモ化

これまで:

function Item({ item, onSelect }: Props) {
  const label = useMemo(() => formatLabel(item), [item]);
  const handleClick = useCallback(() => onSelect(item.id), [onSelect, item.id]);
  return <button onClick={handleClick}>{label}</button>;
}

React Compiler 導入後:

function Item({ item, onSelect }: Props) {
  const label = formatLabel(item);
  const handleClick = () => onSelect(item.id);
  return <button onClick={handleClick}>{label}</button>;
}

useMemo / useCallback を書かなくても、コンパイラが依存関係を解析してメモ化してくれます。

2. Rules of React の検査

React Compiler は「Rules of React」(純粋性、Hooks のルール、不変性) を前提として動作します。eslint-plugin-react-compiler がこのルール違反を検出してくれます。

npm install --save-dev eslint-plugin-react-compiler
{
  "plugins": ["react-compiler"],
  "rules": {
    "react-compiler/react-compiler": "error"
  }
}

3. 既存コードベースへの段階的導入

ディレクトリ単位やファイル単位でオプトインできるため、巨大プロジェクトでも段階的に有効化できます。

4. フレームワーク統合

Next.js、Vite、Expo など主要フレームワークが React Compiler のオプションをドキュメント化しており、最小設定で導入可能です。

5. 開発体験の向上

依存配列の書き忘れ・書き間違いに起因するバグが減り、コードもシンプルになります。

導入手順

Vite + React の場合

npm install --save-dev babel-plugin-react-compiler
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [["babel-plugin-react-compiler", {}]],
      },
    }),
  ],
});

Next.js の場合

npm install --save-dev babel-plugin-react-compiler
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    reactCompiler: true,
  },
};

module.exports = nextConfig;

公式ドキュメントの最新の設定方法に従ってください。フラグ名はバージョンによって変わることがあります。

実践サンプル: リスト表示の最適化

Before

import { useMemo, useCallback } from "react";

type Todo = { id: string; title: string; done: boolean };

export function TodoList({
  todos,
  onToggle,
}: {
  todos: Todo[];
  onToggle: (id: string) => void;
}) {
  const visible = useMemo(() => todos.filter((t) => !t.done), [todos]);
  const handle = useCallback((id: string) => onToggle(id), [onToggle]);

  return (
    <ul>
      {visible.map((t) => (
        <li key={t.id}>
          <button onClick={() => handle(t.id)}>{t.title}</button>
        </li>
      ))}
    </ul>
  );
}

After (React Compiler)

type Todo = { id: string; title: string; done: boolean };

export function TodoList({
  todos,
  onToggle,
}: {
  todos: Todo[];
  onToggle: (id: string) => void;
}) {
  const visible = todos.filter((t) => !t.done);

  return (
    <ul>
      {visible.map((t) => (
        <li key={t.id}>
          <button onClick={() => onToggle(t.id)}>{t.title}</button>
        </li>
      ))}
    </ul>
  );
}

コードがすっきりし、メモ化の漏れによるバグも起きません。

比較表

観点手動メモ化 (従来)React Compiler
記述量多い少ない
漏れ・ミス起きやすい起きにくい
学習コスト
デバッグ自分で追えるコンパイラ依存
既存コード対応都度修正段階導入可能

ベストプラクティス

1. ESLint プラグインを必ず入れる

eslint-plugin-react-compiler の警告ゼロを目標にすれば、コンパイラが安全に動く土台ができます。

2. Rules of React を再確認

「コンポーネントは純粋」「フックはトップレベルで呼ぶ」「props/state は不変」といった基本原則を守っていることが前提です。

3. ディレクトリ単位で段階導入

大規模プロジェクトでは新しい機能ディレクトリから有効化し、既存コードは順次クリーンアップしていきます。

4. プロファイラで効果測定

React DevTools Profiler で再レンダー回数を計測し、導入前後の差分を確認しましょう。

5. メモ化の手書きを基本やめる

導入後はあえて useMemo / useCallback を書かない方針にすることで、コードレビュー負荷も下がります。

注意点・落とし穴

  • 副作用のある関数: 純粋でないコードはコンパイラが正しく扱えない場合があります。副作用は useEffect 内に閉じ込める。
  • クラスコンポーネント非対応: 関数コンポーネント前提の最適化です。
  • デバッグ難度: 自動挿入されたコードを直接読まないと挙動が追えないケースがあるため、Source Map と DevTools を使いこなしましょう。
  • ライブラリ互換性: 一部の独自フックや高度なラッパーで誤検知が出ることがあります。アップデート情報を追う必要があります。
  • 型エラーは別問題: TypeScript の型エラーは React Compiler とは独立しているため、型整備は引き続き重要です。

パフォーマンス観点

  • 再レンダー回数の削減
  • 不要な子コンポーネント更新の抑制
  • バンドルサイズへの影響は限定的 (メモ化コードが追加される分の増加はある)
  • 実アプリでは UX 改善 (入力遅延の低減等) が体感しやすい

FAQ

Q1. すべての useMemo / useCallback を消すべき?

A. 段階的に消していけば OK です。動作確認しながら少しずつ削減していきましょう。

Q2. パフォーマンスは必ず良くなる?

A. ほとんどのケースで改善しますが、極端に最適化された手書きコードからの移行では中立になる場合もあります。

Q3. SSR / RSC とも併用できますか?

A. 可能です。Next.js の App Router など RSC 環境とも統合が進んでいます。

Q4. テストへの影響は?

A. 振る舞いは変わらないため、既存のテストはそのまま動くのが基本です。

Q5. デバッグはどうする?

A. React DevTools と Source Map を活用し、必要に応じて変換前のソースで挙動を追いましょう。

まとめ

React Compiler は、これまで開発者が肩代わりしていた最適化作業を React 自身に戻す大きな一歩です。Rules of React を守った素直なコードを書くだけで、再レンダリング最適化と保守性の高さを両立できます。新規プロジェクトでは積極的に有効化し、既存プロジェクトでは ESLint プラグインから始めて段階的に導入するのがおすすめです。React の書き方そのものをシンプルに戻してくれる、エコシステム全体の前進となる機能です。

より大きなサンプル: 検索付きユーザーテーブル

Before: 手動メモ化の塊

import { useMemo, useCallback, useState } from "react";

type User = { id: string; name: string; email: string; role: "admin" | "user" };

export function UserTable({ users, onDelete }: { users: User[]; onDelete: (id: string) => void }) {
  const [query, setQuery] = useState("");
  const [sortKey, setSortKey] = useState<keyof User>("name");

  const filtered = useMemo(() => {
    const q = query.toLowerCase();
    return users.filter(
      (u) => u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q),
    );
  }, [users, query]);

  const sorted = useMemo(() => {
    return [...filtered].sort((a, b) => String(a[sortKey]).localeCompare(String(b[sortKey])));
  }, [filtered, sortKey]);

  const handleDelete = useCallback(
    (id: string) => {
      if (confirm("削除しますか?")) onDelete(id);
    },
    [onDelete],
  );

  const handleSort = useCallback((key: keyof User) => setSortKey(key), []);

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="検索" />
      <table>
        <thead>
          <tr>
            <th onClick={() => handleSort("name")}>Name</th>
            <th onClick={() => handleSort("email")}>Email</th>
            <th onClick={() => handleSort("role")}>Role</th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          {sorted.map((u) => (
            <tr key={u.id}>
              <td>{u.name}</td>
              <td>{u.email}</td>
              <td>{u.role}</td>
              <td>
                <button onClick={() => handleDelete(u.id)}>削除</button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

After: React Compiler を信頼してシンプルに

import { useState } from "react";

type User = { id: string; name: string; email: string; role: "admin" | "user" };

export function UserTable({ users, onDelete }: { users: User[]; onDelete: (id: string) => void }) {
  const [query, setQuery] = useState("");
  const [sortKey, setSortKey] = useState<keyof User>("name");

  const q = query.toLowerCase();
  const filtered = users.filter(
    (u) => u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q),
  );
  const sorted = [...filtered].sort((a, b) =>
    String(a[sortKey]).localeCompare(String(b[sortKey])),
  );

  const handleDelete = (id: string) => {
    if (confirm("削除しますか?")) onDelete(id);
  };

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="検索" />
      <table>
        <thead>
          <tr>
            <th onClick={() => setSortKey("name")}>Name</th>
            <th onClick={() => setSortKey("email")}>Email</th>
            <th onClick={() => setSortKey("role")}>Role</th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          {sorted.map((u) => (
            <tr key={u.id}>
              <td>{u.name}</td>
              <td>{u.email}</td>
              <td>{u.role}</td>
              <td>
                <button onClick={() => handleDelete(u.id)}>削除</button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

コンポーネントの本質的なロジックだけが残り、依存配列のメンテナンスから解放されます。

ESLint プラグインのルール例

eslint-plugin-react-compiler は次のような違反を検出します。

  • ref を render 中に書き換えている
  • props を直接 mutate している
  • フックを条件分岐の中で呼んでいる
  • レンダー中に setState を無条件で呼んでいる

これらは React 自体のルール違反でもあるため、Compiler の有無に関わらず修正すべきものです。導入の副作用として、コードベース全体の健全性が向上します。

既存プロジェクト移行チェックリスト

  1. React のバージョンが対応版か確認
  2. eslint-plugin-react-compiler を導入し違反を 0 にする
  3. 副作用は useEffect に閉じ込める
  4. props / state を不変に扱う
  5. 1 ディレクトリだけ Compiler を有効化し、回帰テストを実行
  6. React DevTools Profiler で Before/After を測定
  7. 問題なければ全体に展開し、useMemo / useCallback の手書きを段階削減
  8. CI に Profiler ベースの簡易ベンチを入れて再発防止

デバッグのコツ

  • React DevTools の “Highlight updates” で再レンダーを可視化
  • Source Map を有効にして元のコードに対応付け
  • 問題が起きた箇所だけ一時的に Compiler 対象から外して切り分け
  • ライブラリ起因の場合はバージョン固定 + Issue 報告

ライブラリ作者への影響

ライブラリを公開している場合、Compiler が解析しやすい形でフックを書くことが重要になります。具体的には、

  • 引数を mutate しない
  • 戻り値を再利用可能にする
  • 副作用は明示的に useEffect 経由にする
  • ドキュメントに「Rules of React 準拠」を明記する

これらを守ることで、利用者側のプロジェクトで Compiler を有効化したときに問題なく動作します。

よくある質問への深掘り

Q. Profiler で速くなったように見えないことがある

ベンチマーク条件を確認してください。React の StrictMode 下では開発時に意図的に二重レンダーが発生します。本番ビルドで計測しないと効果が見えにくいです。

Q. memo() は不要になる?

ほとんどのケースで不要になりますが、外部から渡される props が頻繁に新規参照になるコンポーネントを「念のため」境界として包む使い方は残ります。

Q. Recoil / Zustand / Jotai との相性は?

これらの状態管理ライブラリは React のルールに従って実装されているため、Compiler とも問題なく併用できます。ただし最新版を使うこと。

Q. Storybook での挙動は?

Storybook 自体のビルドにも Compiler を組み込めます。コンポーネント単位の検証時から最適化済みコードを確認できる利点があります。

アンチパターンとリファクタ例

アンチパターン: render 中の副作用

function Bad({ items }: { items: Item[] }) {
  items.sort((a, b) => a.id - b.id); // 引数を mutate
  return <List items={items} />;
}

修正版

function Good({ items }: { items: Item[] }) {
  const sorted = [...items].sort((a, b) => a.id - b.id);
  return <List items={sorted} />;
}

アンチパターン: render 中の setState

function Bad({ value }: { value: number }) {
  const [doubled, setDoubled] = useState(0);
  setDoubled(value * 2); // ループする
  return <span>{doubled}</span>;
}

修正版

function Good({ value }: { value: number }) {
  const doubled = value * 2;
  return <span>{doubled}</span>;
}

state を計算で置き換えられる場合は、state を持たないのが正解です。

CI への組み込み

name: lint
on: [push, pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npx eslint . --max-warnings 0

eslint-plugin-react-compiler を有効にした状態で --max-warnings 0 を指定し、Compiler が安全に動かせる状態を CI で保証します。

チームへの導入アプローチ

  • ペアプロやモブプロで「もう useMemo を書かない」体験を共有
  • コードレビューのチェック項目から「依存配列の漏れ」を外す
  • 新人向けドキュメントに Rules of React と React Compiler を必須項目として追加
  • 既存コードのリファクタは “ボーイスカウト原則” で触ったついでにクリーンアップ

参考リソース

この技術を体系的に学びたいですか?

未来学では東証プライム上場企業のITエンジニアが24時間サポート。月額24,800円から、退会金0円のオンラインIT塾です。

メールで無料相談する
← 一覧に戻る