React Compiler
Deep dive into react compiler
React Compiler (formerly known as React Forget) is a build-time compiler that automatically memoizes React components and hooks. It analyzes your code and inserts memoization automatically, eliminating the need for manual use of useMemo, useCallback, and React.memo in most cases.
The Problem: Manual Memoization
Before the React Compiler, developers had to manually optimize React applications using memoization hooks:
import { useMemo, useCallback, memo } from 'react';
// Manual memoization required everywhere
const ExpensiveComponent = memo(function ExpensiveComponent({ data, onUpdate }) {
// Memoize expensive computations
const processedData = useMemo(() => {
return data.map(item => heavyProcessing(item));
}, [data]);
// Memoize callbacks to prevent child re-renders
const handleClick = useCallback((id) => {
onUpdate(id);
}, [onUpdate]);
return (
<div>
{processedData.map(item => (
<Item key={item.id} data={item} onClick={handleClick} />
))}
</div>
);
});
Problems with Manual Memoization:
- Cognitive overhead: Developers must understand when and where to memoize
- Dependency array bugs: Missing dependencies cause stale closures
- Code clutter: Hooks add noise to component logic
- Maintenance burden: Refactoring breaks dependency arrays
- Inconsistent application: Easy to miss optimization opportunities
What is React Compiler?
React Compiler is a Babel plugin that runs at build time to:
- Analyze component dependencies: Track which values are used where
- Insert memoization automatically: Add
useMemoanduseCallbackcalls - Ensure correctness: Only memoize when safe to do so
- Preserve semantics: Maintain React’s behavior while optimizing
Key Features
- Automatic memoization: No manual
useMemo/useCallbackneeded - Build-time optimization: Zero runtime overhead from the compiler itself
- Preserves React rules: Works with React’s data flow and hooks rules
- Opt-in adoption: Can be enabled incrementally
- Source map support: Debug compiled code easily
How It Works
The Compilation Process
// Input: Your React component
function UserProfile({ userId, onUpdate }) {
const [data, setData] = useState(null);
useEffect(() => {
fetchUser(userId).then(setData);
}, [userId]);
const fullName = `${data?.firstName} ${data?.lastName}`;
return (
<div onClick={() => onUpdate(userId)}>
<h1>{fullName}</h1>
</div>
);
}
// Output: Automatically memoized (simplified)
function UserProfile({ userId, onUpdate }) {
const [data, setData] = useState(null);
// Compiler inserts memoization
const $ = useMemoCache(5); // React's internal memo cache
useEffect(() => {
fetchUser(userId).then(setData);
}, [userId]);
// Memoized computation
let fullName;
if ($[0] !== data) {
$[0] = data;
$[1] = `${data?.firstName} ${data?.lastName}`;
}
fullName = $[1];
// Memoized callback
const onClick = useCallback(() => onUpdate(userId), [onUpdate, userId]);
return (
<div onClick={onClick}>
<h1>{fullName}</h1>
</div>
);
}
Memoization Strategy
The compiler uses a dependency graph to determine what needs memoization:
function ShoppingCart({ items, currency, onCheckout }) {
// Compiler analyzes dependencies:
// - total depends on: items, currency
// - handleCheckout depends on: onCheckout
const total = items.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0) * getExchangeRate(currency);
const handleCheckout = () => {
onCheckout({ items, total });
};
return (
<div>
<Total amount={total} currency={currency} />
<button onClick={handleCheckout}>Checkout</button>
</div>
);
}
// Compiler output tracks dependencies:
// total: [items, currency]
// handleCheckout: [onCheckout, items, total]
Setting Up React Compiler
Installation
# Install the compiler
npm install babel-plugin-react-compiler
# Or with Vite
npm install -D @react-compiler/vite
Configuration
Babel Configuration:
// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
// Enable strict mode
target: '18',
// Report compilation errors
panicThreshold: 'ALL_ERRORS',
}],
],
};
Vite Configuration:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import reactCompiler from '@react-compiler/vite';
export default defineConfig({
plugins: [
reactCompiler(),
react(),
],
});
Next.js Configuration:
// next.config.js
module.exports = {
experimental: {
reactCompiler: true,
},
};
ESLint Plugin
The compiler includes an ESLint plugin to catch issues:
// .eslintrc.js
module.exports = {
plugins: ['react-compiler'],
rules: {
'react-compiler/react-compiler': 'error',
},
};
Compilation Rules
What Gets Memoized
The compiler memoizes:
- Expensive computations: Array methods, object transformations
- JSX creation: Preventing unnecessary re-renders
- Event handlers: Callbacks passed to child components
- Derived values: Computed properties and transformations
// All of these are automatically memoized
function DataGrid({ rows, columns, onRowClick }) {
// Memoized: filtered and sorted rows
const visibleRows = rows
.filter(row => row.isVisible)
.sort((a, b) => b.timestamp - a.timestamp);
// Memoized: column configuration
const columnConfig = columns.map(col => ({
...col,
width: col.width || 150,
}));
// Memoized: event handler
const handleRowClick = (rowId) => {
onRowClick(rowId);
};
// Memoized: JSX
return (
<table>
{visibleRows.map(row => (
<Row key={row.id} data={row} onClick={handleRowClick} />
))}
</table>
);
}
What Doesn’t Get Memoized
// These won't be memoized (too cheap or unsafe)
function Examples() {
// Primitive literals - no benefit
const count = 42;
// Inline objects passed directly
return <Child config={{ theme: 'dark' }} />; // Compiled differently
// Values that change every render by design
const timestamp = Date.now();
// Side effects in render
console.log('rendering'); // Can't memoize side effects
}
Migration Guide
Gradual Adoption
You can adopt the compiler incrementally:
// Use 'use no memo' to opt-out specific components
function ThirdPartyComponent() {
'use no memo';
// This component won't be compiled
return <div>...</div>;
}
Cleaning Up Manual Memoization
After enabling the compiler, you can remove manual optimizations:
// Before: Manual memoization
function TodoList({ todos, onToggle }) {
const completedCount = useMemo(
() => todos.filter(t => t.completed).length,
[todos]
);
const handleToggle = useCallback(
(id) => onToggle(id),
[onToggle]
);
return (
<div>
<p>Completed: {completedCount}</p>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
/>
))}
</div>
);
}
// After: Compiler handles it automatically
function TodoList({ todos, onToggle }) {
const completedCount = todos.filter(t => t.completed).length;
const handleToggle = (id) => onToggle(id);
return (
<div>
<p>Completed: {completedCount}</p>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
/>
))}
</div>
);
}
Performance Impact
Before and After
// Scenario: Large list with frequent updates
function ProductList({ products, filters, onSelect }) {
// Without compiler: Re-filters on every render
// With compiler: Only re-filters when dependencies change
const filteredProducts = products.filter(p => {
return filters.category === 'all' || p.category === filters.category;
});
// Without compiler: New function every render causes child re-renders
// With compiler: Same function reference when onSelect hasn't changed
const handleSelect = (id) => {
onSelect(id);
};
return (
<ul>
{filteredProducts.map(product => (
<ProductCard
key={product.id}
product={product}
onSelect={handleSelect}
/>
))}
</ul>
);
}
Benchmark Results
| Scenario | Without Compiler | With Compiler | Improvement |
|---|---|---|---|
| Large list filtering | 45ms per render | 2ms per render | 22x faster |
| Deep component tree | 120ms mount | 35ms mount | 3.4x faster |
| Rapid user input | 60fps drops | 60fps stable | Smooth |
Common Patterns
1. List Rendering
function DataTable({ data, sortKey, onSort }) {
// Compiler memoizes the expensive sort
const sortedData = [...data].sort((a, b) => {
return a[sortKey].localeCompare(b[sortKey]);
});
// Compiler memoizes this callback
const handleSort = (key) => {
onSort(key);
};
return (
<table>
<thead>
<tr>
<th onClick={() => handleSort('name')}>Name</th>
<th onClick={() => handleSort('date')}>Date</th>
</tr>
</thead>
<tbody>
{sortedData.map(row => (
<TableRow key={row.id} data={row} />
))}
</tbody>
</table>
);
}
2. Form Handling
function ContactForm({ onSubmit, initialData }) {
const [formData, setFormData] = useState(initialData);
// Compiler memoizes derived validation state
const isValid = formData.email.includes('@') && formData.name.length > 0;
// Compiler memoizes event handlers
const handleChange = (field) => (value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = () => {
if (isValid) onSubmit(formData);
};
return (
<form onSubmit={handleSubmit}>
<Input
value={formData.name}
onChange={handleChange('name')}
/>
<Input
value={formData.email}
onChange={handleChange('email')}
/>
<button disabled={!isValid}>Submit</button>
</form>
);
}
3. Data Fetching
function UserDashboard({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
useEffect(() => {
fetchUser(userId).then(setUser);
fetchPosts(userId).then(setPosts);
}, [userId]);
// Compiler memoizes this computation
const stats = {
postCount: posts.length,
lastActive: posts[0]?.createdAt,
avgLikes: posts.reduce((sum, p) => sum + p.likes, 0) / posts.length || 0,
};
return (
<div>
<UserHeader user={user} />
<StatsPanel stats={stats} />
<PostList posts={posts} />
</div>
);
}
Debugging
React DevTools Integration
The compiler integrates with React DevTools to show:
- Which components were memoized
- Cache hit/miss rates
- Reasons for re-renders
Compiler Playground
Use the online playground to see compiled output: https://react-compiler-playground.vercel.app
// Input
function MyComponent({ data }) {
const processed = data.map(x => x * 2);
return <div>{processed.join(', ')}</div>;
}
// See the compiled output with memoization inserted
Comparison with Manual Memoization
| Aspect | Manual useMemo/useCallback | React Compiler |
|---|---|---|
| Setup | Manual code changes | Build configuration |
| Maintenance | Update dependency arrays | Automatic updates |
| Correctness | Human error possible | Guaranteed correct |
| Granularity | Coarse (whole hooks) | Fine-grained (individual values) |
| Bundle size | Hooks add code | Compiled code can be smaller |
| Learning curve | Must learn patterns | Works with existing code |
React Compiler automatically memoizes your React components at build time, eliminating the need for manual useMemo, useCallback, and React.memo. It analyzes component dependencies and inserts optimizations safely. Configure it as a Babel plugin or use the framework-specific plugins for Vite/Next.js. The compiler preserves React semantics while improving performance automatically.