最終更新:
React Compiler実践ガイド: 自動メモ化で実現するパフォーマンス最適化の新時代
React Compiler実践ガイド: 自動メモ化で実現するパフォーマンス最適化の新時代
React Compilerは、手動のuseMemoやuseCallbackを不要にする革新的なコンパイラです。
本記事では、React Compilerの仕組み、導入方法、実践的な最適化テクニック、ベンチマーク、トラブルシューティングまで徹底解説します。
React Compilerとは
従来のパフォーマンス最適化の課題
// 従来: 手動でメモ化が必要
function TodoList({ todos, filter }) {
// これを忘れると毎回再計算
const filteredTodos = useMemo(() => {
return todos.filter(todo => {
if (filter === 'completed') return todo.completed;
if (filter === 'active') return !todo.completed;
return true;
});
}, [todos, filter]);
// これも忘れると子コンポーネントが再レンダリング
const handleToggle = useCallback((id) => {
// ...
}, []);
return (
<div>
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
/>
))}
</div>
);
}
// さらに子コンポーネントもメモ化
const TodoItem = memo(({ todo, onToggle }) => {
return (
<div onClick={() => onToggle(todo.id)}>
{todo.text}
</div>
);
});
問題点:
- メンテナンス負担: 依存配列の管理が煩雑
- パフォーマンスリスク: メモ化忘れによる無駄な再レンダリング
- コード可読性低下: 最適化コードがロジックを隠す
- 学習コスト: 初心者には難解
React Compilerの革新
// React Compiler: 自動で最適化される
function TodoList({ todos, filter }) {
// コンパイラが自動でメモ化
const filteredTodos = todos.filter(todo => {
if (filter === 'completed') return todo.completed;
if (filter === 'active') return !todo.completed;
return true;
});
// これも自動で最適化
const handleToggle = (id) => {
// ...
};
return (
<div>
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
/>
))}
</div>
);
}
// memo()も不要
function TodoItem({ todo, onToggle }) {
return (
<div onClick={() => onToggle(todo.id)}>
{todo.text}
</div>
);
}
メリット:
- 自動最適化: コンパイラが賢く判断
- シンプルなコード: メモ化のボイラープレート不要
- 保守性向上: 依存配列の管理から解放
- パフォーマンス向上: 最適化の抜け漏れ防止
React Compilerの仕組み
コンパイル時の変換
React Compilerは、コード内の以下を自動検出します:
- 値の再計算: 高コストな処理を自動でメモ化
- 関数の再生成: コールバックを自動で安定化
- コンポーネントの再レンダリング: 不要な再描画を防止
- JSXの再構築: 仮想DOMの差分を最小化
内部動作の例
// 元のコード
function Component({ data }) {
const processed = data.map(item => item.value * 2);
return <List items={processed} />;
}
コンパイラによる変換(イメージ):
// コンパイル後(概念的な例)
function Component({ data }) {
const processed = useMemoInternal(() => {
return data.map(item => item.value * 2);
}, [data]);
const jsx = useMemoInternal(() => {
return <List items={processed} />;
}, [processed]);
return jsx;
}
導入方法
前提条件
- React 19以降
- Node.js 18以降
- Babel 7.24以降 または ESBuild/SWC対応版
インストール(Next.js)
npm install next@latest react@latest react-dom@latest
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
reactCompiler: true,
},
};
module.exports = nextConfig;
インストール(Vite)
npm install vite-plugin-react-compiler -D
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import reactCompiler from 'vite-plugin-react-compiler';
export default defineConfig({
plugins: [
react(),
reactCompiler(),
],
});
インストール(CRA / カスタムWebpack)
npm install babel-plugin-react-compiler -D
// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
// オプション設定
}],
],
};
ESLintプラグイン
npm install eslint-plugin-react-compiler -D
// .eslintrc.js
module.exports = {
plugins: ['react-compiler'],
rules: {
'react-compiler/react-compiler': 'error',
},
};
実践的な最適化例
1. リスト処理の最適化
// Before: 手動メモ化
function ProductList({ products, searchTerm, sortBy }) {
const filtered = useMemo(() => {
return products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [products, searchTerm]);
const sorted = useMemo(() => {
return [...filtered].sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
if (sortBy === 'name') return a.name.localeCompare(b.name);
return 0;
});
}, [filtered, sortBy]);
return (
<div>
{sorted.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// After: React Compilerが自動最適化
function ProductList({ products, searchTerm, sortBy }) {
const filtered = products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const sorted = [...filtered].sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
if (sortBy === 'name') return a.name.localeCompare(b.name);
return 0;
});
return (
<div>
{sorted.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
2. イベントハンドラの最適化
// Before: useCallbackだらけ
function Form({ onSubmit, initialValues }) {
const [values, setValues] = useState(initialValues);
const handleChange = useCallback((field, value) => {
setValues(prev => ({ ...prev, [field]: value }));
}, []);
const handleSubmit = useCallback((e) => {
e.preventDefault();
onSubmit(values);
}, [values, onSubmit]);
const handleReset = useCallback(() => {
setValues(initialValues);
}, [initialValues]);
return (
<form onSubmit={handleSubmit}>
<Input
value={values.name}
onChange={(v) => handleChange('name', v)}
/>
<Button type="submit">Submit</Button>
<Button onClick={handleReset}>Reset</Button>
</form>
);
}
// After: シンプルに
function Form({ onSubmit, initialValues }) {
const [values, setValues] = useState(initialValues);
const handleChange = (field, value) => {
setValues(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(values);
};
const handleReset = () => {
setValues(initialValues);
};
return (
<form onSubmit={handleSubmit}>
<Input
value={values.name}
onChange={(v) => handleChange('name', v)}
/>
<Button type="submit">Submit</Button>
<Button onClick={handleReset}>Reset</Button>
</form>
);
}
3. 複雑な計算の最適化
// Before: ネストしたuseMemo
function Dashboard({ users, orders, startDate, endDate }) {
const filteredOrders = useMemo(() => {
return orders.filter(o =>
o.date >= startDate && o.date <= endDate
);
}, [orders, startDate, endDate]);
const stats = useMemo(() => {
const revenue = filteredOrders.reduce((sum, o) => sum + o.total, 0);
const avgOrderValue = revenue / filteredOrders.length;
const topCustomers = calculateTopCustomers(filteredOrders, users);
return { revenue, avgOrderValue, topCustomers };
}, [filteredOrders, users]);
const chartData = useMemo(() => {
return prepareChartData(filteredOrders);
}, [filteredOrders]);
return (
<div>
<Stats data={stats} />
<Chart data={chartData} />
</div>
);
}
// After: クリーンなロジック
function Dashboard({ users, orders, startDate, endDate }) {
const filteredOrders = orders.filter(o =>
o.date >= startDate && o.date <= endDate
);
const revenue = filteredOrders.reduce((sum, o) => sum + o.total, 0);
const avgOrderValue = revenue / filteredOrders.length;
const topCustomers = calculateTopCustomers(filteredOrders, users);
const stats = { revenue, avgOrderValue, topCustomers };
const chartData = prepareChartData(filteredOrders);
return (
<div>
<Stats data={stats} />
<Chart data={chartData} />
</div>
);
}
4. Context使用時の最適化
// Before: memo()とuseCallbackが必要
const UserContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const login = useCallback((credentials) => {
// API call
setUser(userData);
}, []);
const logout = useCallback(() => {
setUser(null);
}, []);
const value = useMemo(() => ({
user,
login,
logout,
}), [user, login, logout]);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
const UserProfile = memo(function UserProfile() {
const { user } = useContext(UserContext);
return <div>{user?.name}</div>;
});
// After: 自然な記述
const UserContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const login = (credentials) => {
// API call
setUser(userData);
};
const logout = () => {
setUser(null);
};
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
}
function UserProfile() {
const { user } = useContext(UserContext);
return <div>{user?.name}</div>;
}
最適化の限界と注意点
Compilerが最適化できないケース
// 1. 副作用を含む処理
function Component({ data }) {
// これは最適化されない(毎回実行される)
const result = data.map(item => {
console.log(item); // 副作用
return item * 2;
});
return <div>{result}</div>;
}
// 2. ランダム値や日時
function Component() {
// 毎回異なる値になるため最適化不可
const random = Math.random();
const now = new Date();
return <div>{random}</div>;
}
// 3. 外部変数の参照
let globalCounter = 0;
function Component({ data }) {
// グローバル変数の変更は追跡不可
globalCounter++;
return <div>{globalCounter}</div>;
}
手動最適化が必要な場合
// 明示的に最適化を抑制したい場合
function Component({ data }) {
// 'use no memo' ディレクティブ(将来的な機能)
'use no memo';
const result = expensiveOperation(data);
return <div>{result}</div>;
}
パフォーマンス計測
React DevToolsでの確認
// Profilerでレンダリング回数を計測
import { Profiler } from 'react';
function App() {
return (
<Profiler
id="TodoList"
onRender={(id, phase, actualDuration) => {
console.log(`${id} rendered in ${actualDuration}ms`);
}}
>
<TodoList />
</Profiler>
);
}
ベンチマーク例
// Before Compiler: 100アイテムのリスト
// - 平均レンダリング時間: 45ms
// - 再レンダリング回数: 検索ごとに全リスト
// After Compiler: 100アイテムのリスト
// - 平均レンダリング時間: 12ms (73%改善)
// - 再レンダリング回数: 変更されたアイテムのみ
実際の測定コード
function BenchmarkComponent() {
const [data, setData] = useState(generateLargeData(1000));
const [filter, setFilter] = useState('');
const start = performance.now();
const filtered = data.filter(item =>
item.name.includes(filter)
);
const end = performance.now();
console.log(`Filter took ${end - start}ms`);
return (
<div>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
/>
{filtered.map(item => (
<Item key={item.id} item={item} />
))}
</div>
);
}
トラブルシューティング
ESLintエラーの対処
// react-compiler/react-compiler エラーが出る場合
// 原因: Compilerが最適化できないパターン
// ❌ Bad
function Component({ data }) {
let sum = 0;
data.forEach(item => sum += item.value); // ミュータブルな変数
return <div>{sum}</div>;
}
// ✅ Good
function Component({ data }) {
const sum = data.reduce((acc, item) => acc + item.value, 0);
return <div>{sum}</div>;
}
ビルドエラーの対処
# Compilerのバージョンを確認
npm list babel-plugin-react-compiler
# キャッシュクリア
rm -rf node_modules/.cache
npm run build
デバッグモード
// next.config.js
const nextConfig = {
experimental: {
reactCompiler: {
compilationMode: 'annotation', // 'all' | 'annotation'
},
},
};
// 特定コンポーネントのみ有効化
'use memo';
function Component() {
// このコンポーネントのみCompiler適用
}
マイグレーション戦略
段階的導入
// Step 1: 新規コンポーネントのみ有効化
const nextConfig = {
experimental: {
reactCompiler: {
compilationMode: 'annotation',
},
},
};
// Step 2: アノテーション追加
'use memo';
function NewComponent() {
// React Compilerが適用される
}
既存コードのリファクタリング
// Before
const Component = memo(function Component({ data }) {
const processed = useMemo(() => process(data), [data]);
const handler = useCallback(() => {}, []);
return <div>{processed}</div>;
});
// After: 段階的に削除
function Component({ data }) {
const processed = process(data);
const handler = () => {};
return <div>{processed}</div>;
}
まとめ
React Compilerの実践的な活用方法を解説しました。
キーポイント
- 自動最適化: useMemo/useCallback/memo不要
- シンプルなコード: 可読性とパフォーマンスの両立
- 段階的導入: 既存プロジェクトにも適用可能
- ESLint連携: 最適化不可パターンを検出
導入のベストプラクティス
- 新規プロジェクトから: フルに活用
- 既存プロジェクト: annotationモードで段階的に
- ESLint設定: 早期にエラーを検出
- 計測: React DevToolsでパフォーマンス確認
React Compilerで、よりシンプルで高速なReactアプリケーションを開発しましょう。