React Compiler完全ガイド2026:Reactアプリケーション開発の変革
React Compilerは開発者がReactアプリケーションを書く方法を根本的に変えました。かつて実験的な技術だったものが、今では2026年に高性能Reactアプリを構築するための推奨アプローチとなっています。この包括的なガイドでは、React Compilerについて知っておくべきすべて—基本概念から高度な移行戦略まで—をカバーします。
新しいプロジェクトを開始する場合でも、既存のコードベースを移行する場合でも、このガイドはReact Compilerの自動最適化機能を活用するために必要な実践的な知識を提供します。
React Compilerとは?
React Compiler(以前はReact Forgetとして知られていた)は、有益な場所にメモ化を自動的に挿入してReactコンポーネントを最適化するahead-of-timeコンパイラです。開発者が手動で値をuseMemoやuseCallbackでラップする代わりに、コンパイラがコードを分析し、これらの最適化を自動的に追加します。
解決するコア問題
Reactのレンダリングモデルは、状態が変更されるとコンポーネントサブツリー全体を再レンダリングします。これにより、パフォーマンスを低下させる不必要な再レンダリングが発生します。開発者は従来、手動メモ化でこれを解決していました:
// 従来の手動最適化アプローチ
function ProductList({ products, onSelect }) {
// 手動でメモ化されたソート済み製品
const sortedProducts = useMemo(() => {
return [...products].sort((a, b) => a.name.localeCompare(b.name));
}, [products]);
// 手動でメモ化されたコールバック
const handleSelect = useCallback((product) => {
onSelect(product.id);
}, [onSelect]);
// 手動でメモ化されたフィルター済みリスト
const expensiveFiltered = useMemo(() => {
return sortedProducts.filter(p => p.price > 100);
}, [sortedProducts]);
return (
<ul>
{expensiveFiltered.map(product => (
<ProductItem
key={product.id}
product={product}
onSelect={handleSelect}
/>
))}
</ul>
);
}
このアプローチには重大な問題があります:
- 開発者の負担:何をメモ化するか決定するには専門知識が必要
- 過剰メモ化:不必要なメモ化がオーバーヘッドを追加
- メモ化不足:最適化の欠落がパフォーマンスを損なう
- メンテナンスコスト:依存配列を同期させ続ける必要がある
React Compilerがこれをどう解決するか
React Compilerを使用すると、同じコンポーネントを自然に書くことができます:
// React Compiler使用 - 自然なコードを書く
function ProductList({ products, onSelect }) {
const sortedProducts = [...products].sort((a, b) =>
a.name.localeCompare(b.name)
);
const handleSelect = (product) => {
onSelect(product.id);
};
const expensiveFiltered = sortedProducts.filter(p => p.price > 100);
return (
<ul>
{expensiveFiltered.map(product => (
<ProductItem
key={product.id}
product={product}
onSelect={handleSelect}
/>
))}
</ul>
);
}
コンパイラはこのコードを分析し、最適化が有益と判断した場所に自動的にメモ化を挿入します。コンパイル出力には、開発者の介入なしに必要なuseMemoとuseCallback呼び出しが含まれます。
React Compilerの仕組み
コンパイラの動作を理解することで、最適化されやすいコードを書くことができます。技術的な基盤を探りましょう。
静的解析とコンパイル
React Compilerはいくつかの分析パスを実行します:
1. 依存グラフの構築
コンパイラはコンポーネント内のすべての値とその依存関係のグラフを構築します:
コンポーネント: ProductList
├── sortedProducts
│ └── depends on: products
├── handleSelect
│ └── depends on: onSelect
└── expensiveFiltered
└── depends on: sortedProducts
2. リアクティビティ推論
コンパイラは「リアクティブ」な値(レンダー間で変更される可能性がある)を判断します:
リアクティブ値:
- products(prop - リアクティブ)
- onSelect(prop - リアクティブ)
- sortedProducts(productsから派生 - リアクティブ)
- handleSelect(onSelectを参照 - リアクティブ)
- expensiveFiltered(sortedProductsから派生 - リアクティブ)
3. メモ化の挿入
分析に基づいて、コンパイラはメモ化を挿入します:
// コンパイラ出力(簡略化された表現)
function ProductList({ products, onSelect }) {
const sortedProducts = useMemo(
() => [...products].sort((a, b) => a.name.localeCompare(b.name)),
[products]
);
const handleSelect = useCallback(
(product) => onSelect(product.id),
[onSelect]
);
const expensiveFiltered = useMemo(
() => sortedProducts.filter(p => p.price > 100),
[sortedProducts]
);
return useMemo(() => (
<ul>
{expensiveFiltered.map(product => (
<ProductItem
key={product.id}
product={product}
onSelect={handleSelect}
/>
))}
</ul>
), [expensiveFiltered, handleSelect]);
}
Reactのルール
React CompilerはReactのルール—予測可能なコンポーネント動作を保証する一連の制約—を強制します。これらのルールを理解することは、コンパイラ互換コードを書くために不可欠です。
ルール1:コンポーネントとHooksは純粋でなければならない
コンポーネントは同じ入力が与えられると同じ出力を生成する必要があります:
// ✅ 純粋 - 決定論的な出力
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
// ❌ 不純 - 出力が外部状態に依存
let visitCount = 0;
function Greeting({ name }) {
visitCount++; // レンダー中の副作用
return <h1>Hello, {name}! Visit #{visitCount}</h1>;
}
ルール2:Reactがコンポーネントとフックを呼び出す
コンポーネントを通常の関数として呼び出さないでください:
// ✅ 正しい - Reactがコンポーネントを呼び出す
function App() {
return <UserProfile userId={123} />;
}
// ❌ 不正 - コンポーネントを関数として呼び出し
function App() {
return UserProfile({ userId: 123 }); // コンパイラ最適化を壊す
}
ルール3:フックのルール
フックはトップレベルで、同じ順序で呼び出す必要があります:
// ✅ 正しい - フックはトップレベル
function Profile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return loading ? <Spinner /> : <UserCard user={user} />;
}
// ❌ 不正 - 条件付きフック
function Profile({ userId }) {
if (!userId) return null;
const [user, setUser] = useState(null); // 早期リターン後のフック
// ...
}
React Compilerのセットアップ
プロジェクトでReact Compilerをセットアップする手順を見ていきましょう。
前提条件
React Compilerには以下が必要です:
- React 19以降
- Node.js 18+
- Babelまたは互換性のあるビルドツール
インストール
ステップ1:コンパイラパッケージをインストール
npm install react@19 react-dom@19
npm install -D babel-plugin-react-compiler
ステップ2:Babelを設定
コンパイラプラグインをBabel設定に追加:
{
"plugins": [
["babel-plugin-react-compiler", {
"sources": (filename) => {
return filename.includes("src/");
}
}]
]
}
ステップ3:特定のフレームワーク用に設定
Next.js(14.3以降)の場合:
// next.config.js
const nextConfig = {
experimental: {
reactCompiler: true,
},
};
module.exports = nextConfig;
Viteの場合:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import reactCompiler from 'babel-plugin-react-compiler';
export default defineConfig({
plugins: [
react({
babel: {
plugins: [reactCompiler],
},
}),
],
});
段階的採用戦略
既存のコードベースでは、コンパイラを段階的に採用してください:
フェーズ1:新しいコードのみに有効化
// babel.config.js
module.exports = {
plugins: [
["babel-plugin-react-compiler", {
sources: (filename) => {
// 新機能ディレクトリのみコンパイル
return filename.includes("src/features/new-dashboard/");
}
}]
]
};
フェーズ2:より多くのディレクトリに拡張
sources: (filename) => {
return (
filename.includes("src/features/new-dashboard/") ||
filename.includes("src/features/checkout/") ||
filename.includes("src/components/v2/")
);
}
フェーズ3:コードベース全体
sources: (filename) => {
// 既知の問題ファイルを除外
const excludePatterns = [
"legacy/",
"vendor/",
"__tests__/"
];
return !excludePatterns.some(pattern => filename.includes(pattern));
}
移行ガイド:手動メモ化からReact Compilerへ
既存のコードベースの移行には慎重な計画が必要です。このセクションでは一般的なパターンとその対処方法をカバーします。
パターン1:useMemoとuseCallbackの削除
最も簡単な移行は不必要なメモ化の削除です:
移行前:
function SearchResults({ query, filters }) {
const filteredResults = useMemo(() => {
return searchData
.filter(item => item.title.includes(query))
.filter(item => filters.every(f => item.tags.includes(f)));
}, [query, filters]);
const handleItemClick = useCallback((item) => {
analytics.track('search_result_click', { itemId: item.id });
navigate(`/items/${item.id}`);
}, [navigate]);
return (
<ResultsList
results={filteredResults}
onItemClick={handleItemClick}
/>
);
}
移行後(React Compiler):
function SearchResults({ query, filters }) {
const filteredResults = searchData
.filter(item => item.title.includes(query))
.filter(item => filters.every(f => item.tags.includes(f)));
const handleItemClick = (item) => {
analytics.track('search_result_click', { itemId: item.id });
navigate(`/items/${item.id}`);
};
return (
<ResultsList
results={filteredResults}
onItemClick={handleItemClick}
/>
);
}
パターン2:React.memoの処理
React.memoはuseMemoとは異なる目的を持ちます—コンポーネントの再レンダリングを防ぎます。React Compilerはほとんどの場合これを自動的に処理します:
移行前:
const ProductCard = React.memo(function ProductCard({ product, onSelect }) {
return (
<div className="product-card" onClick={() => onSelect(product)}>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
});
移行後:
// React Compilerが自動的に再レンダリングを最適化
function ProductCard({ product, onSelect }) {
return (
<div className="product-card" onClick={() => onSelect(product)}>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
}
パターン3:複雑な依存チェーン
コンパイラは複雑な依存関係の追跡に優れています:
移行前:
function Dashboard({ userId, dateRange }) {
const user = useMemo(() => users.find(u => u.id === userId), [userId]);
const permissions = useMemo(() =>
calculatePermissions(user?.role),
[user]
);
const visibleWidgets = useMemo(() =>
widgets.filter(w => permissions.canView(w.type)),
[permissions]
);
const widgetData = useMemo(() =>
visibleWidgets.map(w => ({
...w,
data: fetchWidgetData(w.id, dateRange)
})),
[visibleWidgets, dateRange]
);
return <WidgetGrid widgets={widgetData} />;
}
移行後:
function Dashboard({ userId, dateRange }) {
const user = users.find(u => u.id === userId);
const permissions = calculatePermissions(user?.role);
const visibleWidgets = widgets.filter(w =>
permissions.canView(w.type)
);
const widgetData = visibleWidgets.map(w => ({
...w,
data: fetchWidgetData(w.id, dateRange)
}));
return <WidgetGrid widgets={widgetData} />;
}
パターン4:カスタムフック
メモ化された値を返すカスタムフックは簡略化できます:
移行前:
function useFilteredData(items, filterFn) {
const filtered = useMemo(() =>
items.filter(filterFn),
[items, filterFn]
);
const stats = useMemo(() => ({
total: items.length,
filtered: filtered.length,
percentage: (filtered.length / items.length * 100).toFixed(1)
}), [items.length, filtered.length]);
return { filtered, stats };
}
移行後:
function useFilteredData(items, filterFn) {
const filtered = items.filter(filterFn);
const stats = {
total: items.length,
filtered: filtered.length,
percentage: (filtered.length / items.length * 100).toFixed(1)
};
return { filtered, stats };
}
高度なパターンとエッジケース
すべてのコードパターンがReact Compilerでスムーズに動作するわけではありません。これらのエッジケースを理解することで、互換性のあるコードを書くことができます。
エッジケース1:ミュータブル参照
コンパイラは値が不変であると仮定します。ミュータブルなパターンは問題を引き起こします:
// ❌ 問題あり - レンダー中のミューテーション
function Counter() {
let count = 0;
const increment = () => {
count++; // ミューテーションは適切に追跡されない
forceUpdate();
};
return <button onClick={increment}>{count}</button>;
}
// ✅ 正しい - stateを使用
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(c => c + 1);
};
return <button onClick={increment}>{count}</button>;
}
エッジケース2:外部ミュータブル状態
ミュータブルな外部状態への参照にはuse no memoディレクティブが必要です:
// 外部ミュータブルシングルトン
const globalCache = new Map();
function CachedComponent({ id }) {
'use no memo'; // コンパイルをオプトアウト
if (!globalCache.has(id)) {
globalCache.set(id, computeExpensiveValue(id));
}
return <div>{globalCache.get(id)}</div>;
}
エッジケース3:動的プロパティアクセス
一部の動的パターンはコンパイラを混乱させます:
// ❌ 動的プロパティアクセスは最適化されにくい
function DynamicComponent({ type, data }) {
const Component = componentMap[type]; // 動的ルックアップ
return <Component data={data} />;
}
// ✅ より良い - 明示的なマッピング
function DynamicComponent({ type, data }) {
switch (type) {
case 'chart':
return <ChartComponent data={data} />;
case 'table':
return <TableComponent data={data} />;
case 'list':
return <ListComponent data={data} />;
default:
return <DefaultComponent data={data} />;
}
}
エッジケース4:クラスコンポーネント
クラスコンポーネントはコンパイルされません:
// クラスコンポーネントはコンパイラをバイパス
class LegacyComponent extends React.Component {
render() {
// React Compilerによって最適化されない
return <div>{this.props.value}</div>;
}
}
// 最適化のために関数コンポーネントに変換
function ModernComponent({ value }) {
// React Compilerによって最適化
return <div>{value}</div>;
}
パフォーマンス最適化ベストプラクティス
コンパイラはメモ化を自動的に処理しますが、これらのプラクティスに従うことで最適なパフォーマンスが保証されます。
プラクティス1:静的データを引き上げる
静的データをコンポーネント外に移動:
// ❌ 毎回のレンダーで再作成
function CountrySelector({ selected, onChange }) {
const countries = [ // 各レンダーで配列が再作成
{ code: 'US', name: 'United States' },
{ code: 'UK', name: 'United Kingdom' },
// ...
];
return <Select options={countries} value={selected} onChange={onChange} />;
}
// ✅ 静的データを引き上げ
const COUNTRIES = [
{ code: 'US', name: 'United States' },
{ code: 'UK', name: 'United Kingdom' },
// ...
];
function CountrySelector({ selected, onChange }) {
return <Select options={COUNTRIES} value={selected} onChange={onChange} />;
}
プラクティス2:派生状態をコロケート
派生計算を使用場所の近くに保持:
// ✅ 良い - 派生状態が使用場所の近く
function ProductPage({ product }) {
// 派生値が必要な場所で計算
const isOnSale = product.salePrice < product.originalPrice;
const discount = isOnSale
? Math.round((1 - product.salePrice / product.originalPrice) * 100)
: 0;
return (
<div>
<h1>{product.name}</h1>
{isOnSale && <Badge>{discount}% OFF</Badge>}
<Price value={product.salePrice} original={product.originalPrice} />
</div>
);
}
プラクティス3:Propsでのインラインオブジェクト作成を避ける
JSXの前にオブジェクト/配列を作成:
// ❌ インラインオブジェクト - 各レンダーで新しい参照
function StyledButton({ children }) {
return (
<Button style={{ backgroundColor: 'blue', padding: 16 }}>
{children}
</Button>
);
}
// ✅ JSXの前にオブジェクトを定義
function StyledButton({ children }) {
const buttonStyle = { backgroundColor: 'blue', padding: 16 };
return (
<Button style={buttonStyle}>
{children}
</Button>
);
}
// ✅ または静的スタイルを使用
const BUTTON_STYLE = { backgroundColor: 'blue', padding: 16 };
function StyledButton({ children }) {
return (
<Button style={BUTTON_STYLE}>
{children}
</Button>
);
}
プラクティス4:Fragmentを賢く使用
Fragmentはコンパイラがコンポーネント構造を理解するのに役立ちます:
// ✅ DOMノードなしでグループ化するためのFragment
function UserInfo({ user }) {
return (
<>
<Avatar src={user.avatar} />
<UserName name={user.name} />
<UserStatus status={user.status} />
</>
);
}
デバッグとトラブルシューティング
React Compilerには最適化問題をデバッグするためのツールが含まれています。
React DevTools Compilerタブの使用
React DevTools(v5.0以降)にはCompilerタブが含まれており、以下を表示:
- どのコンポーネントがコンパイルされているか
- どの最適化が適用されたか
- 特定のコードが最適化されなかった理由
ESLintプラグイン
eslint-plugin-react-compilerは問題を検出するのに役立ちます:
// eslint.config.js
import reactCompiler from 'eslint-plugin-react-compiler';
export default [
{
plugins: {
'react-compiler': reactCompiler,
},
rules: {
'react-compiler/react-compiler': 'error',
},
},
];
一般的な警告メッセージ
「最適化できません:コンポーネントにレンダー中の副作用があります」
// ❌ レンダー中の副作用
function LoggingComponent({ value }) {
console.log('Rendering with:', value); // 副作用
return <div>{value}</div>;
}
// ✅ 副作用にはuseEffectを使用
function LoggingComponent({ value }) {
useEffect(() => {
console.log('Rendered with:', value);
}, [value]);
return <div>{value}</div>;
}
「最適化できません:非決定論的な値」
// ❌ 非決定論的
function RandomGreeting({ name }) {
const greeting = Math.random() > 0.5 ? 'Hello' : 'Hi';
return <span>{greeting}, {name}!</span>;
}
// ✅ stateで決定論的に
function RandomGreeting({ name }) {
const [greeting] = useState(() =>
Math.random() > 0.5 ? 'Hello' : 'Hi'
);
return <span>{greeting}, {name}!</span>;
}
React Compiler最適化のテスト
コンパイラ最適化がアプリケーションで正しく動作することを確認。
コンパイル済みコンポーネントのユニットテスト
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('ProductList with React Compiler', () => {
it('製品を正しくレンダリング', () => {
const products = [
{ id: 1, name: 'Product A' },
{ id: 2, name: 'Product B' },
];
render(<ProductList products={products} onSelect={jest.fn()} />);
expect(screen.getByText('Product A')).toBeInTheDocument();
expect(screen.getByText('Product B')).toBeInTheDocument();
});
it('選択を正しく処理', async () => {
const onSelect = jest.fn();
const products = [{ id: 1, name: 'Product A' }];
render(<ProductList products={products} onSelect={onSelect} />);
await userEvent.click(screen.getByText('Product A'));
expect(onSelect).toHaveBeenCalledWith(1);
});
});
パフォーマンステスト
import { Profiler } from 'react';
function measureRenders(id, phase, actualDuration) {
console.log(`${id} ${phase}: ${actualDuration.toFixed(2)}ms`);
}
function App() {
return (
<Profiler id="ProductList" onRender={measureRenders}>
<ProductList products={products} onSelect={handleSelect} />
</Profiler>
);
}
実世界の移行ケーススタディ
複雑なeコマースコンポーネントの実践的な移行を見てみましょう。
移行前:手動最適化
function ShoppingCart({ items, discounts, user }) {
// 複数の手動メモ化呼び出し
const subtotal = useMemo(() =>
items.reduce((sum, item) => sum + item.price * item.quantity, 0),
[items]
);
const applicableDiscounts = useMemo(() =>
discounts.filter(d => d.minPurchase <= subtotal && d.isActive),
[discounts, subtotal]
);
const bestDiscount = useMemo(() =>
applicableDiscounts.reduce((best, d) =>
d.percentage > (best?.percentage || 0) ? d : best,
null
),
[applicableDiscounts]
);
const total = useMemo(() =>
bestDiscount
? subtotal * (1 - bestDiscount.percentage / 100)
: subtotal,
[subtotal, bestDiscount]
);
const handleCheckout = useCallback(() => {
checkout({ items, total, userId: user.id, discountCode: bestDiscount?.code });
}, [items, total, user.id, bestDiscount]);
const handleRemoveItem = useCallback((itemId) => {
removeFromCart(itemId);
}, []);
const handleUpdateQuantity = useCallback((itemId, quantity) => {
updateCartItem(itemId, quantity);
}, []);
return (
<div className="shopping-cart">
<CartItems
items={items}
onRemove={handleRemoveItem}
onUpdateQuantity={handleUpdateQuantity}
/>
<CartSummary
subtotal={subtotal}
discount={bestDiscount}
total={total}
/>
<CheckoutButton onClick={handleCheckout} disabled={items.length === 0} />
</div>
);
}
移行後:React Compiler
function ShoppingCart({ items, discounts, user }) {
// クリーンで読みやすい計算
const subtotal = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const applicableDiscounts = discounts.filter(
d => d.minPurchase <= subtotal && d.isActive
);
const bestDiscount = applicableDiscounts.reduce(
(best, d) => d.percentage > (best?.percentage || 0) ? d : best,
null
);
const total = bestDiscount
? subtotal * (1 - bestDiscount.percentage / 100)
: subtotal;
// クリーンなイベントハンドラー
const handleCheckout = () => {
checkout({
items,
total,
userId: user.id,
discountCode: bestDiscount?.code
});
};
const handleRemoveItem = (itemId) => {
removeFromCart(itemId);
};
const handleUpdateQuantity = (itemId, quantity) => {
updateCartItem(itemId, quantity);
};
return (
<div className="shopping-cart">
<CartItems
items={items}
onRemove={handleRemoveItem}
onUpdateQuantity={handleUpdateQuantity}
/>
<CartSummary
subtotal={subtotal}
discount={bestDiscount}
total={total}
/>
<CheckoutButton onClick={handleCheckout} disabled={items.length === 0} />
</div>
);
}
結果:
- コード複雑度40%削減
- レンダリングパフォーマンス15%向上
- 読みやすく保守しやすい
- データパターンが変わっても自動最適化
結論:React開発の未来
React CompilerはReact開発哲学の根本的なシフトを表しています:自然なコードを書き、コンパイラに最適化させる。
重要なポイント
- 手動メモ化を削除:コンパイラに
useMemoとuseCallbackを処理させる - Reactのルールに従う:純粋なコンポーネントと適切なフック使用が最適化を可能にする
- 段階的に採用:新しいコードから始め、既存のコードベースに拡張
- ツールを使用:ESLintプラグインとDevToolsが問題の特定に役立つ
- 可読性に集中:クリーンなコードは賢いコードよりも最適化される
今後の展望
Reactエコシステムが進化し続ける中、コンパイラはさらに洗練されていきます。今日必要に思えるパターンも、明日には自動的に最適化されるでしょう。最善のアプローチは、クリーンでイディオマティックなReactコードを書き、コンパイラがそれを高速にすることを信頼することです。
React開発の専門知識が必要ですか?
最新のコンパイラ最適化を使用した現代のReactアプリケーションを構築するには、Reactの基礎と最先端のツールの両方を理解した経験豊富な開発者が必要です。当社のオフショア開発チームは高性能Reactアプリケーションを専門としています。
Reactコードベースを現代化する準備はできていますか?React Compilerは、よりクリーンなコードでより良いパフォーマンスへの道です。
React Compilerへの移行でどのような課題に直面していますか?あなたの経験と質問を共有してください。
出典: