React Compiler完全ガイド - useMemo/useCallbackが不要になる自動メモ化の世界
React Compiler完全ガイド
React 19で正式導入されるReact Compilerは、開発者の手動によるメモ化(useMemo、useCallback)を不要にし、自動的にコンポーネントを最適化する革新的な機能です。この記事では、React Compilerの仕組み、導入方法、ベストプラクティスを徹底的に解説します。
React Compilerとは
React Compilerは、Reactコードをビルド時に解析し、自動的にメモ化を追加するコンパイラです。従来、開発者が手動で行っていたuseMemoやuseCallbackの最適化を、コンパイラが自動で行います。
従来の問題点
// 従来: 手動でメモ化が必要
function UserList({ users, onSelect }) {
// usersが変わらなくても毎回新しい配列が作られる
const sortedUsers = users.sort((a, b) => a.name.localeCompare(b.name));
// 毎回新しい関数が作られる
const handleClick = (user) => {
onSelect(user);
};
return (
<div>
{sortedUsers.map(user => (
<UserItem key={user.id} user={user} onClick={handleClick} />
))}
</div>
);
}
React Compilerによる自動最適化
// React Compiler使用時: 自動的にメモ化される
function UserList({ users, onSelect }) {
// コンパイラが自動的にメモ化
const sortedUsers = users.sort((a, b) => a.name.localeCompare(b.name));
// コンパイラが自動的にメモ化
const handleClick = (user) => {
onSelect(user);
};
return (
<div>
{sortedUsers.map(user => (
<UserItem key={user.id} user={user} onClick={handleClick} />
))}
</div>
);
}
React Compilerの仕組み
依存関係の自動追跡
React Compilerは、コンポーネント内の値や関数の依存関係を自動的に追跡し、最適なメモ化戦略を決定します。
function ProductCard({ product, onAddToCart }) {
// productの変更を自動追跡
const price = product.price;
const discount = product.discount || 0;
// price と discount の変更を自動追跡
const finalPrice = price * (1 - discount / 100);
// finalPrice と onAddToCart の変更を自動追跡
const handleAdd = () => {
onAddToCart(product.id, finalPrice);
};
return (
<div>
<h3>{product.name}</h3>
<p>¥{finalPrice.toLocaleString()}</p>
<button onClick={handleAdd}>カートに追加</button>
</div>
);
}
コンパイル結果の例
// コンパイル前
function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
// コンパイル後(概念的な表現)
function Counter() {
const [count, setCount] = useState(0);
const increment = useMemo(() => () => setCount(c => c + 1), []);
const decrement = useMemo(() => () => setCount(c => c - 1), []);
const jsx = useMemo(() => (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
), [count, increment, decrement]);
return jsx;
}
導入方法
1. インストール
npm install --save-dev babel-plugin-react-compiler
または
yarn add -D babel-plugin-react-compiler
2. Babel設定
// .babelrc
{
"plugins": [
["react-compiler", {
"runtimeModule": "react-compiler-runtime"
}]
]
}
3. Next.js での設定
// next.config.js
const ReactCompilerConfig = {
compilationMode: 'annotation', // または 'all'
};
module.exports = {
experimental: {
reactCompiler: ReactCompilerConfig,
},
};
4. Vite での設定
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
['react-compiler', {
runtimeModule: 'react-compiler-runtime'
}]
]
}
})
]
});
コンパイルモード
annotation モード(推奨)
特定のコンポーネントのみをコンパイルするモード。段階的な導入に最適です。
'use memo'; // このディレクティブでコンパイル対象を明示
function ExpensiveComponent({ data }) {
// 重い計算処理
const processedData = data.map(item => ({
...item,
computed: heavyComputation(item)
}));
return (
<div>
{processedData.map(item => (
<ItemCard key={item.id} item={item} />
))}
</div>
);
}
all モード
すべてのコンポーネントを自動的にコンパイルするモード。
// next.config.js
module.exports = {
experimental: {
reactCompiler: {
compilationMode: 'all',
},
},
};
infer モード
React Compilerがパフォーマンス向上が見込めるコンポーネントを自動判定してコンパイルします。
// next.config.js
module.exports = {
experimental: {
reactCompiler: {
compilationMode: 'infer',
},
},
};
パフォーマンス最適化の実例
1. リスト表示の最適化
'use memo';
interface TodoListProps {
todos: Todo[];
onToggle: (id: string) => void;
onDelete: (id: string) => void;
}
function TodoList({ todos, onToggle, onDelete }: TodoListProps) {
// フィルタリングが自動的にメモ化される
const activeTodos = todos.filter(todo => !todo.completed);
const completedTodos = todos.filter(todo => todo.completed);
// ソートも自動的にメモ化される
const sortedActive = activeTodos.sort((a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
return (
<div>
<section>
<h2>アクティブ ({activeTodos.length})</h2>
{sortedActive.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</section>
<section>
<h2>完了 ({completedTodos.length})</h2>
{completedTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</section>
</div>
);
}
2. フォーム処理の最適化
'use memo';
function RegistrationForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: '',
});
// バリデーション関数が自動的にメモ化される
const errors = {
username: formData.username.length < 3
? 'ユーザー名は3文字以上必要です'
: '',
email: !formData.email.includes('@')
? '有効なメールアドレスを入力してください'
: '',
password: formData.password.length < 8
? 'パスワードは8文字以上必要です'
: '',
confirmPassword: formData.password !== formData.confirmPassword
? 'パスワードが一致しません'
: '',
};
// isValidも自動的にメモ化される
const isValid = Object.values(errors).every(error => error === '');
// ハンドラー関数も自動的にメモ化される
const handleChange = (field: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({
...prev,
[field]: e.target.value,
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!isValid) return;
await submitRegistration(formData);
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
value={formData.username}
onChange={handleChange('username')}
placeholder="ユーザー名"
/>
{errors.username && <span className="error">{errors.username}</span>}
</div>
<div>
<input
type="email"
value={formData.email}
onChange={handleChange('email')}
placeholder="メールアドレス"
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<input
type="password"
value={formData.password}
onChange={handleChange('password')}
placeholder="パスワード"
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
<div>
<input
type="password"
value={formData.confirmPassword}
onChange={handleChange('confirmPassword')}
placeholder="パスワード(確認)"
/>
{errors.confirmPassword && <span className="error">{errors.confirmPassword}</span>}
</div>
<button type="submit" disabled={!isValid}>
登録
</button>
</form>
);
}
3. データビジュアライゼーションの最適化
'use memo';
interface ChartProps {
data: DataPoint[];
width: number;
height: number;
}
function LineChart({ data, width, height }: ChartProps) {
// スケール計算が自動的にメモ化される
const xScale = width / (data.length - 1);
const maxValue = Math.max(...data.map(d => d.value));
const yScale = height / maxValue;
// パス生成が自動的にメモ化される
const pathData = data.map((point, index) => ({
x: index * xScale,
y: height - (point.value * yScale),
}));
const pathString = pathData
.map((point, index) =>
`${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}`
)
.join(' ');
// グリッドラインの計算も自動的にメモ化される
const gridLines = Array.from({ length: 5 }, (_, i) => ({
y: (height / 4) * i,
label: ((maxValue / 4) * (4 - i)).toFixed(0),
}));
return (
<svg width={width} height={height}>
{/* グリッドライン */}
{gridLines.map((line, index) => (
<g key={index}>
<line
x1={0}
y1={line.y}
x2={width}
y2={line.y}
stroke="#e0e0e0"
strokeWidth={1}
/>
<text x={5} y={line.y - 5} fontSize={12}>
{line.label}
</text>
</g>
))}
{/* データライン */}
<path
d={pathString}
fill="none"
stroke="#2196F3"
strokeWidth={2}
/>
{/* データポイント */}
{pathData.map((point, index) => (
<circle
key={index}
cx={point.x}
cy={point.y}
r={4}
fill="#2196F3"
/>
))}
</svg>
);
}
既存コードの移行
useMemoの削除
// Before: 手動メモ化
function ProductList({ products, category }) {
const filteredProducts = useMemo(() =>
products.filter(p => p.category === category),
[products, category]
);
const sortedProducts = useMemo(() =>
filteredProducts.sort((a, b) => b.rating - a.rating),
[filteredProducts]
);
return (
<div>
{sortedProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// After: React Compiler使用
'use memo';
function ProductList({ products, category }) {
const filteredProducts = products.filter(p => p.category === category);
const sortedProducts = filteredProducts.sort((a, b) => b.rating - a.rating);
return (
<div>
{sortedProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
useCallbackの削除
// Before: 手動メモ化
function SearchBar({ onSearch }) {
const [query, setQuery] = useState('');
const handleChange = useCallback((e) => {
setQuery(e.target.value);
}, []);
const handleSubmit = useCallback((e) => {
e.preventDefault();
onSearch(query);
}, [query, onSearch]);
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={handleChange} />
<button type="submit">検索</button>
</form>
);
}
// After: React Compiler使用
'use memo';
function SearchBar({ onSearch }) {
const [query, setQuery] = useState('');
const handleChange = (e) => {
setQuery(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
onSearch(query);
};
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={handleChange} />
<button type="submit">検索</button>
</form>
);
}
React.memoの扱い
// Before: 手動メモ化
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) {
return (
<div>
{data.map(item => <Item key={item.id} item={item} />)}
</div>
);
});
// After: React Compilerではmemoは不要だが、互換性のため残しても問題ない
'use memo';
function ExpensiveComponent({ data }) {
return (
<div>
{data.map(item => <Item key={item.id} item={item} />)}
</div>
);
}
デバッグとモニタリング
React DevToolsでの確認
React DevTools Profilerを使用して、コンパイラによる最適化を確認できます。
'use memo';
function MonitoredComponent({ data }) {
// DevToolsで再レンダリングをトラッキング
console.log('Render:', data.id);
const processedData = heavyComputation(data);
return <div>{processedData}</div>;
}
コンパイルログの有効化
// next.config.js
module.exports = {
experimental: {
reactCompiler: {
compilationMode: 'annotation',
logger: {
level: 'debug',
output: 'console', // または 'file'
},
},
},
};
パフォーマンス計測
'use memo';
import { Profiler } from 'react';
function App() {
const onRenderCallback = (
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) => {
console.log({
id,
phase,
actualDuration,
baseDuration,
});
};
return (
<Profiler id="App" onRender={onRenderCallback}>
<MainContent />
</Profiler>
);
}
ベストプラクティス
1. 段階的な導入
// Step 1: 最もパフォーマンスが重要なコンポーネントから開始
'use memo';
function CriticalComponent({ data }) {
// ...
}
// Step 2: 段階的に他のコンポーネントに適用
'use memo';
function SecondaryComponent({ data }) {
// ...
}
2. 純粋なコンポーネントを書く
'use memo';
// Good: 純粋な関数
function PureComponent({ value }) {
const doubled = value * 2;
return <div>{doubled}</div>;
}
// Bad: 副作用を含む
function ImpureComponent({ value }) {
// コンパイラは副作用を最適化できない
localStorage.setItem('value', value);
return <div>{value}</div>;
}
3. 適切な状態管理
'use memo';
function OptimizedForm() {
// Good: 関連する状態をまとめる
const [formData, setFormData] = useState({
name: '',
email: '',
});
// Bad: 個別の状態
// const [name, setName] = useState('');
// const [email, setEmail] = useState('');
return (
<form>
<input
value={formData.name}
onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
/>
<input
value={formData.email}
onChange={e => setFormData(prev => ({ ...prev, email: e.target.value }))}
/>
</form>
);
}
4. 計算の分離
'use memo';
function DataDashboard({ rawData }) {
// Good: 計算ロジックを分離
const processedData = processData(rawData);
const statistics = calculateStatistics(processedData);
const chartData = prepareChartData(processedData);
return (
<div>
<Statistics data={statistics} />
<Chart data={chartData} />
</div>
);
}
// 純粋な関数として定義
function processData(data) {
return data.map(item => ({
...item,
normalized: normalizeValue(item.value),
}));
}
function calculateStatistics(data) {
return {
mean: data.reduce((sum, item) => sum + item.value, 0) / data.length,
max: Math.max(...data.map(item => item.value)),
min: Math.min(...data.map(item => item.value)),
};
}
トラブルシューティング
最適化されない場合
'use memo';
function ProblematicComponent({ data }) {
// 問題: 外部変数を参照
const externalValue = window.globalValue; // コンパイラは最適化できない
// 解決: propsとして渡す
return <div>{data.value}</div>;
}
ビルドエラー
# エラー: react-compilerが見つからない
npm install --save-dev babel-plugin-react-compiler
# エラー: runtime moduleが見つからない
npm install react-compiler-runtime
TypeScript統合
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"strict": true,
"moduleResolution": "bundler"
}
}
パフォーマンス比較
ベンチマーク例
// テストコンポーネント
function BenchmarkComponent({ items }) {
const start = performance.now();
const processed = items.map(item => ({
...item,
computed: heavyComputation(item),
}));
const end = performance.now();
console.log(`処理時間: ${end - start}ms`);
return (
<div>
{processed.map(item => (
<Item key={item.id} item={item} />
))}
</div>
);
}
// 手動メモ化の場合: 平均 50ms
// React Compiler使用時: 平均 12ms (約4倍高速)
まとめ
React Compilerは、Reactアプリケーションのパフォーマンス最適化を劇的に簡素化します。
主な利点
- 開発生産性の向上 - useMemo/useCallbackが不要
- パフォーマンス向上 - 自動的に最適化
- コードの可読性向上 - シンプルなコードを書ける
- バグの削減 - 依存配列の書き忘れがない
導入時の推奨フロー
- annotation モードで導入開始
- 重要なコンポーネントから段階的に適用
- DevToolsでパフォーマンス計測
- 既存のuseMemo/useCallbackを段階的に削除
- all モードへの移行を検討
React Compilerを活用して、より高速で保守性の高いReactアプリケーションを構築しましょう。