Back

React Compiler徹底解説:自動メモ化でパフォーマンス最適化作業の90%を削減する方法

useMemouseCallbackReact.memoの泥沼にはまって、アプリがカクつく原因の無駄な再レンダリングを追いかけた経験、ありませんか?長年にわたり、Reactのパフォーマンス最適化は誰もが通る道でした——試行錯誤の連続です。

その時代が終わりを迎えようとしています。

2025年末に安定版となったReact Compilerは、Reactのパフォーマンスについての考え方を根本的に変えます。メモ化が必要なすべての関数と値に手動でアノテーションを付ける代わりに、コンパイラがビルド時にコードを分析し、実際に必要な場所に自動的に最適化を挿入します。

この包括的なガイドでは、React Compilerがどのように動作するか深く掘り下げ、自動メモ化の技術的基盤を探り、既存のコードベースの移行戦略を説明し、本番アプリケーションからの実際のパフォーマンスベンチマークを検証します。最後まで読めば、React Compilerの使い方だけでなく、なぜこれがHooks以来のReact史上最も重要な進化なのかを理解できるでしょう。


React Compilerが解決するパフォーマンス問題

再レンダリング税

Reactの宣言的モデルは強力です。UIがどのように見えるべきかを記述すれば、ReactがDOMをどう更新するか判断してくれます。しかし、この力にはコストが伴います——Reactは状態変更のたびにコンポーネントを再レンダリングし、その再レンダリングはコンポーネントツリーを下にカスケードしていきます。

function ParentComponent() { const [count, setCount] = useState(0); // このオブジェクトは毎回のレンダリングで再作成される const config = { theme: 'dark', locale: 'ja' }; // この関数も毎回のレンダリングで再作成される const handleClick = () => { console.log('clicked'); }; return ( <div> <Counter count={count} setCount={setCount} /> {/* config/handleClickが「実際には」変わっていなくてもChildComponentは再レンダリングされる */} <ChildComponent config={config} onClick={handleClick} /> </div> ); }

countが変わるたびに、ChildComponentは新しいオブジェクトと関数の参照を受け取ります——実際の値は同一であっても。

従来のメモ化の調整

従来の解決策は手動メモ化でした:

function ParentComponent() { const [count, setCount] = useState(0); const config = useMemo(() => ({ theme: 'dark', locale: 'ja' }), []); const handleClick = useCallback(() => console.log('clicked'), []); return ( <div> <Counter count={count} setCount={setCount} /> <MemoizedChildComponent config={config} onClick={handleClick} /> </div> ); } const MemoizedChildComponent = React.memo(ChildComponent);

これは機能しますが、いくつかの問題を引き起こします:

  1. 考えることが多すぎる:参照等価性について常に意識する必要がある
  2. 過剰なメモ化:すべてをラップすることで不要な複雑さが増す
  3. 不十分なメモ化:一つのメモ化を見逃すと最適化チェーン全体が崩壊
  4. 依存配列地獄:誤った配列はstaleなクロージャや不要な再計算につながる
  5. メンテナンス負担:コードが進化するにつれてメモ化パターンも更新が必要

Meta社は、**パフォーマンス問題の60〜70%**がメモ化の欠落または誤りに関連していると推定しました。


React Compilerの仕組み

React Compilerは魔法ではありません——ビルドプロセス中に実行される洗練された静的分析ツールです。

アーキテクチャ

コンパイラはビルドステップでReactコードを変換するBabelプラグインとして動作します:

ソースコード → パーサー → AST → コンパイラ分析 → 最適化されたコード → バンドル

3つの主要フェーズ:

  1. 分析:コードをASTにパースしてデータフローを分析
  2. 推論:どの値がリアクティブか静的かを判定
  3. 変換:最適なポイントにメモ化を挿入

リアクティビティモデル

コンパイラは各式を以下のように分類します:

  • 静的:変更されない値(定数、imports)
  • リアクティブ:props、state、またはcontextに依存する値
  • 派生:他のリアクティブ値から計算される値
function ProductCard({ product, discount }) { // 静的:変更されない const formatter = new Intl.NumberFormat('ja-JP'); const MAX_TITLE_LENGTH = 50; // リアクティブ:propsへの直接依存 const price = product.price; const name = product.name; // 派生:リアクティブ値から計算 const finalPrice = price * (1 - discount); const displayTitle = name.slice(0, MAX_TITLE_LENGTH); return ( <div className="product-card"> <h3>{displayTitle}</h3> <span>{formatter.format(finalPrice)}</span> </div> ); }

コンパイラは自動的に以下を判断します:

  • formatterMAX_TITLE_LENGTHはコンポーネント外にホイストできる
  • finalPricepricediscountが変わった時だけ再計算が必要
  • displayTitlenameが変わった時だけ再計算が必要

きめ細かいリアクティビティ(Fine-Grained Reactivity)

function UserDashboard({ user }) { const { name, email, preferences, stats } = user; return ( <div> <Header name={name} /> <EmailSettings email={email} preferences={preferences} /> <StatsPanel stats={stats} /> </div> ); }

コンパイラは以下を理解します:

  • Headernameのみに依存
  • EmailSettingsemailpreferencesに依存
  • StatsPanelstatsのみに依存

statsだけが変更された場合、コンパイルされたコードはHeaderEmailSettingsの再レンダリングを完全にスキップします。


React Compilerのセットアップ

前提条件

  • React 19以上
  • Node.js 18+
  • BabelまたはSWCプラグインをサポートするビルドツール

Viteでのインストール

npm install -D babel-plugin-react-compiler @babel/core @babel/preset-react
// vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [ react({ babel: { plugins: [ ['babel-plugin-react-compiler', { runtimeModule: 'react/compiler-runtime', }], ], }, }), ], });

Next.jsでのインストール

Next.js 16以上はファーストパーティサポートがあります:

// next.config.js const nextConfig = { experimental: { reactCompiler: true, }, }; module.exports = nextConfig;

ファイル単位で有効化することも可能です:

'use compiler'; export default function OptimizedComponent() { // このコンポーネントはコンパイルされます }

Expoでのインストール

Expo SDK 54以上はReact Compilerがデフォルトで有効です。以前のSDKの場合:

// babel.config.js module.exports = function (api) { api.cache(true); return { presets: ['babel-preset-expo'], plugins: [ ['babel-plugin-react-compiler', { runtimeModule: 'react/compiler-runtime', }], ], }; };

移行戦略

戦略1: ディレクトリ単位の段階的導入

{ sources: (filename) => { return filename.includes('src/components/dashboard'); }, }

戦略2: ファイル単位のオプトイン

'use compiler'; export function HighPerformanceList({ items }) { return items.map(item => <ListItem key={item.id} item={item} />); }

オプトアウトも可能:

'use no compiler'; export function LegacyComponent() { // リファクタリングが必要なレガシーコード }

戦略3: ESLintによる事前準備

npm install -D eslint-plugin-react-hooks@latest
{ "rules": { "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "error", "react-compiler/react-compiler": "error" } }

手動メモ化のクリーンアップ

// Before: 手動最適化 function BeforeComponent({ data, onSelect }) { const processedData = useMemo(() => data.map(item => ({ ...item, processed: true })), [data] ); const handleSelect = useCallback((id) => { onSelect(id); }, [onSelect]); return <MemoizedList items={processedData} onItemSelect={handleSelect} />; } // After: コンパイラに任せる function AfterComponent({ data, onSelect }) { const processedData = data.map(item => ({ ...item, processed: true })); const handleSelect = (id) => onSelect(id); return <List items={processedData} onItemSelect={handleSelect} />; }

実際のパフォーマンスベンチマーク

Metaの内部テスト

メトリクス改善
初期読み込み時間12%高速化
ユーザーインタラクション速度2.5倍高速化
再レンダリング回数60%削減
バンドルサイズニュートラル

Sanity Studio

  • レンダリング時間20〜30%削減
  • 複雑なエディターでのレイテンシ大幅改善
  • 即座の生産性向上(デバッグ時間削減)

Wakelet Core Web Vitals

メトリクスBeforeAfter改善
LCP2.4s1.8s25%高速化
INP180ms95ms47%高速化
CLS0.050.0340%改善

DIYベンチマーキング

import { Profiler } from 'react'; function onRenderCallback(id, phase, actualDuration, baseDuration) { console.log({ component: id, phase, actualDuration: `${actualDuration.toFixed(2)}ms`, baseDuration: `${baseDuration.toFixed(2)}ms`, }); } function App() { return ( <Profiler id="App" onRender={onRenderCallback}> <YourApplication /> </Profiler> ); }

上級コンパイラパターン

パターン1: 静的計算の抽出

// ❌ 混在 function ProductPage({ product }) { const formatPrice = (price) => { const formatter = new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }); return formatter.format(price); }; return <span>{formatPrice(product.price)}</span>; } // ✅ 抽出済み const priceFormatter = new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }); function ProductPage({ product }) { return <span>{priceFormatter.format(product.price)}</span>; }

パターン2: レンダリング中の副作用を避ける

// ❌ レンダリング中の副作用 function AnalyticsWrapper({ children, pageId }) { trackPageView(pageId); // 副作用! return <div>{children}</div>; } // ✅ useEffectで副作用 function AnalyticsWrapper({ children, pageId }) { useEffect(() => { trackPageView(pageId); }, [pageId]); return <div>{children}</div>; }

パターン3: イミュータブルな配列操作

// ❌ ミューテーション function SortedList({ items }) { const sorted = items; sorted.sort((a, b) => a.name.localeCompare(b.name)); // ミューテート! return sorted.map(item => <Item key={item.id} {...item} />); } // ✅ イミュータブル function SortedList({ items }) { const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name)); return sorted.map(item => <Item key={item.id} {...item} />); } // ✅ より良い: toSorted (ES2023+) function SortedList({ items }) { const sorted = items.toSorted((a, b) => a.name.localeCompare(b.name)); return sorted.map(item => <Item key={item.id} {...item} />); }

パターン4: Refの正しい扱い

function InputWithFocus({ onSubmit }) { const inputRef = useRef(null); const handleSubmit = () => { const value = inputRef.current?.value; onSubmit(value); }; return ( <form onSubmit={handleSubmit}> <input ref={inputRef} /> <button type="submit">送信</button> </form> ); }

手動メモ化が必要な場合

明示的な依存関係

function DataFetcher({ userId, options }) { const fetchData = useCallback(async () => { const response = await fetch(`/api/users/${userId}`); return response.json(); }, [userId]); // optionsを明示的に無視 useEffect(() => { fetchData(); }, [fetchData]); }

サードパーティライブラリ連携

function ChartComponent({ data }) { const xScale = useMemo(() => d3.scaleLinear().domain([0, d3.max(data)]).range([0, 500]), [data] ); const handleZoom = useCallback((event) => { xScale.domain(event.transform.rescaleX(xScale).domain()); }, [xScale]); }

一般的な落とし穴とデバッグ

落とし穴1: コンパイラのベイルアウト

// ❌ 動的プロパティアクセス - コンパイラがベイルアウトする可能性 function DynamicComponent({ data, fieldName }) { return <span>{data[fieldName]}</span>; } // ✅ 明示的なプロパティ function ExplicitComponent({ data }) { return <span>{data.name}</span>; }

落とし穴2: 外部ミューテーション

// ❌ 親が配列をミューテート function Parent() { const items = [1, 2, 3]; const addItem = () => { items.push(4); // ミューテーション! setTrigger(x => !x); }; return <Child items={items} />; } // ✅ 適切な状態管理 function Parent() { const [items, setItems] = useState([1, 2, 3]); const addItem = () => { setItems(prev => [...prev, 4]); }; return <Child items={items} />; }

コンパイル出力のデバッグ

{ plugins: [ ['babel-plugin-react-compiler', { debug: true, }], ], }

出力:

[ReactCompiler] Analyzing: ProductCard
  - Variables: 5 total (2 static, 2 reactive, 1 derived)
  - Memoization points: 3
  - JSX fragments: 2 (1 optimized)
  Estimated re-render reduction: 78%

Reactパフォーマンスの未来

シグナルベースのリアクティビティ

// 将来の潜在的なAPI(推測) function Counter() { const count = useSignal(0); return ( <button onClick={() => count.value++}> Count: {count.value} </button> ); }

Server Componentsとの連携

コンパイラはReact Server Componentsとシームレスに動作するよう設計されています。サーバーコンポーネントはコンパイラを完全にスキップし(クライアントサイドのリアクティビティが不要なため)、クライアントコンポーネントは完全な最適化を受けます。


まとめ

React Compilerは単なるパフォーマンスツールではありません——Reactが最適化を扱う方法における哲学的な転換です。開発者に手動メモ化の決定を押し付ける代わりに、Reactがコンポーネントを可能な限り効率的にする責任を負うようになりました。

キーポイント:

  1. コンパイラはビルド時に動作し、コードを分析して実際の効果がある場所にメモ化を挿入します
  2. 移行は段階的に可能 — 影響の大きいディレクトリや個別ファイルから始められます
  3. コンパイラフレンドリーなコードを書くために、レンダリング時の副作用やミューテーションを避けましょう
  4. 手動メモ化にも役割があります — 特定のシナリオのためのエスケープハッチとして
  5. パフォーマンス向上は実際のもの — コード変更なしで再レンダリングが20〜60%減少することが期待できます

useMemouseCallbackの不安の時代は終わりを迎えています。React Compilerがあれば、本当に重要なこと——素晴らしいユーザー体験を構築すること——に集中できます。

今日からコンパイラを試してみてください。未来のあなた——そしてユーザー——が感謝するでしょう。

ReactReact CompilerPerformanceMemoizationJavaScriptFrontend