React Compiler: What It Actually Does to Your Code
React Compiler: What It Actually Does to Your Code
A deep technical breakdown of what the new React compiler memoizes, what it can't handle, how it transforms your components under the hood, and what it means for how you write components going forward.
What The Compiler Actually Is
The React Compiler (previously called "React Forget") is a build-time compiler that automatically memoizes React components and hooks. It's not a runtime optimization — it transforms your source code during the build process.
┌─────────────────────────────────────────────────────────────────────────────┐
│ WHERE THE COMPILER FITS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ WITHOUT COMPILER: │
│ ───────────────── │
│ Source Code → Babel/SWC → Bundler → Browser │
│ │
│ WITH COMPILER: │
│ ────────────── │
│ Source Code → React Compiler → Babel/SWC → Bundler → Browser │
│ │ │
│ └── Analyzes component │
│ Inserts memoization │
│ Adds cache slots │
│ │
│ The compiler is a BABEL PLUGIN (or SWC equivalent). │
│ It runs at build time, not runtime. │
│ Your shipped code is different from your source code. │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
The key insight: you write code as if there's no memoization, and the compiler adds it for you.
The Problem It Solves
React's rendering model re-runs your entire component function on every state change. Without memoization, this means:
function ProductPage({ productId }) {
// ALL of this runs on EVERY render:
// 1. This object is recreated
const filters = { inStock: true, minRating: 4 };
// 2. This array is recreated
const sortOptions = ['price', 'rating', 'newest'];
// 3. This function is recreated
const handleAddToCart = () => {
addToCart(productId);
};
// 4. This expensive computation runs again
const recommendations = products
.filter(p => p.category === product.category)
.sort((a, b) => b.rating - a.rating)
.slice(0, 10);
return (
<div>
{/* 5. These children re-render because props are new references */}
<ProductFilters filters={filters} />
<SortDropdown options={sortOptions} />
<AddToCartButton onClick={handleAddToCart} />
<Recommendations items={recommendations} />
</div>
);
}
Before the compiler, you fixed this manually:
function ProductPage({ productId }) {
// Manual memoization everywhere
const filters = useMemo(() => ({ inStock: true, minRating: 4 }), []);
const sortOptions = useMemo(() => ['price', 'rating', 'newest'], []);
const handleAddToCart = useCallback(() => addToCart(productId), [productId]);
const recommendations = useMemo(() =>
products.filter(/*...*/).sort(/*...*/).slice(0, 10),
[products, product.category]
);
return (/* ... */);
}
This is tedious, error-prone (wrong dependency arrays), and clutters your code. The compiler automates all of it.
How The Compiler Analyzes Your Code
Static Single Assignment (SSA) Form
The compiler first converts your component into SSA form — a representation where each variable is assigned exactly once.
// Your code
function Component({ items }) {
let total = 0;
for (const item of items) {
total += item.price;
}
return <Display value={total} />;
}
// Conceptual SSA form (simplified)
function Component({ items }) {
const total_0 = 0;
const total_1 = total_0 + items[0].price;
const total_2 = total_1 + items[1].price;
// ... and so on
const total_final = /* last assignment */;
return <Display value={total_final} />;
}
This form makes it easy to track data flow — which values depend on which inputs.
Dependency Graph Construction
The compiler builds a graph of dependencies:
┌─────────────────────────────────────────────────────────────────────────────┐
│ DEPENDENCY GRAPH EXAMPLE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ function Component({ userId, theme }) { │
│ const user = useUser(userId); │
│ const posts = usePosts(userId); │
│ const styles = getStyles(theme); │
│ const displayName = formatName(user.name); │
│ const postCount = posts.length; │
│ │
│ return ( │
│ <Card style={styles}> │
│ <Header name={displayName} count={postCount} /> │
│ <PostList posts={posts} /> │
│ </Card> │
│ ); │
│ } │
│ │
│ Dependency graph: │
│ │
│ userId ─────┬─────▶ user ──────────▶ displayName ────┐ │
│ │ │ │
│ └─────▶ posts ─────┬──▶ postCount ───────┼──▶ <Header /> │
│ │ │ │
│ └──────────────────────┼──▶ <PostList /> │
│ │ │
│ theme ─────────────▶ styles ──────────────────────────┴──▶ <Card /> │
│ │
│ The compiler sees: │
│ • If userId changes: user, posts, displayName, postCount all invalidate │
│ • If theme changes: only styles invalidates │
│ • <Header /> depends on displayName + postCount │
│ • <PostList /> depends only on posts │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Reactivity Analysis
The compiler categorizes every value:
┌─────────────────────────────────────────────────────────────────────────────┐
│ VALUE CATEGORIES │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ REACTIVE VALUES (change between renders): │
│ ────────────────────────────────────────── │
│ • Props │
│ • State (useState, useReducer) │
│ • Context values │
│ • Values derived from reactive values │
│ • Hook return values │
│ │
│ NON-REACTIVE VALUES (stable across renders): │
│ ───────────────────────────────────────────── │
│ • Primitive literals (42, "hello", true) │
│ • Module-level constants │
│ • Functions defined outside component │
│ • Refs (the ref object itself, not .current) │
│ • Stable hook returns (dispatch from useReducer) │
│ │
│ The compiler only memoizes computations involving reactive values. │
│ Non-reactive values don't need memoization — they're already stable. │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
The Transformation: Before and After
Simple Object Memoization
// YOUR CODE
function Component({ color }) {
const style = { backgroundColor: color, padding: 20 };
return <div style={style} />;
}
// COMPILER OUTPUT (conceptual)
function Component({ color }) {
const $ = useMemoCache(2); // Request 2 cache slots
let style;
if ($[0] !== color) { // Has color changed?
style = { backgroundColor: color, padding: 20 };
$[0] = color; // Store dependency
$[1] = style; // Store result
} else {
style = $[1]; // Reuse cached result
}
return <div style={style} />;
}
Function Memoization
// YOUR CODE
function Component({ items, onSelect }) {
const handleClick = (id) => {
onSelect(id);
};
return items.map(item => (
<Item key={item.id} onClick={() => handleClick(item.id)} />
));
}
// COMPILER OUTPUT (conceptual)
function Component({ items, onSelect }) {
const $ = useMemoCache(4);
let handleClick;
if ($[0] !== onSelect) {
handleClick = (id) => {
onSelect(id);
};
$[0] = onSelect;
$[1] = handleClick;
} else {
handleClick = $[1];
}
let mappedItems;
// The compiler recognizes that the map depends on items AND handleClick
if ($[2] !== items || $[3] !== handleClick) {
mappedItems = items.map(item => (
<Item key={item.id} onClick={() => handleClick(item.id)} />
));
$[2] = items;
$[3] = mappedItems;
// Note: handleClick is tracked but not stored again — it's in $[1]
} else {
mappedItems = $[3];
}
return mappedItems;
}
JSX Element Memoization
// YOUR CODE
function Component({ user, theme }) {
return (
<Card theme={theme}>
<Header user={user} />
<Footer /> {/* No props — always stable */}
</Card>
);
}
// COMPILER OUTPUT (conceptual)
function Component({ user, theme }) {
const $ = useMemoCache(4);
let header;
if ($[0] !== user) {
header = <Header user={user} />;
$[0] = user;
$[1] = header;
} else {
header = $[1];
}
// Footer has no dependencies — cached once forever
let footer;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
footer = <Footer />;
$[2] = footer;
} else {
footer = $[2];
}
let card;
if ($[3] !== theme || $[1] !== header) {
card = (
<Card theme={theme}>
{header}
{footer}
</Card>
);
$[3] = card;
} else {
card = $[3];
}
return card;
}
Hook Dependency Tracking
// YOUR CODE
function Component({ userId }) {
const [filter, setFilter] = useState('all');
useEffect(() => {
analytics.track('view', { userId, filter });
}, [userId, filter]);
const filteredData = useMemo(() => {
return fetchData(userId).filter(d => d.type === filter);
}, [userId, filter]);
return <Display data={filteredData} />;
}
// COMPILER OUTPUT (conceptual)
function Component({ userId }) {
const $ = useMemoCache(6);
const [filter, setFilter] = useState('all');
// The compiler knows useEffect depends on userId and filter
// It generates a stable dependency array
let effect_deps;
if ($[0] !== userId || $[1] !== filter) {
effect_deps = [userId, filter];
$[0] = userId;
$[1] = filter;
$[2] = effect_deps;
} else {
effect_deps = $[2];
}
useEffect(() => {
analytics.track('view', { userId, filter });
}, effect_deps);
// useMemo — compiler validates your deps are correct
// and may add missing ones or remove unnecessary ones
let filteredData;
if ($[3] !== userId || $[4] !== filter) {
filteredData = fetchData(userId).filter(d => d.type === filter);
$[3] = userId;
$[4] = filter;
$[5] = filteredData;
} else {
filteredData = $[5];
}
return <Display data={filteredData} />;
}
The useMemoCache Hook
The compiler inserts a special hook that manages the cache:
// This is what the compiler generates
function useMemoCache(size: number): Array<unknown> {
// Gets or creates a cache array for this component instance
// The cache persists across renders (like useRef)
// Each slot stores either a value or a sentinel
}
// Sentinel value used to detect "never been set"
const SENTINEL = Symbol.for('react.memo_cache_sentinel');
// Simplified implementation concept
function useMemoCache(size) {
const cache = useRef(null);
if (cache.current === null) {
cache.current = new Array(size).fill(SENTINEL);
}
return cache.current;
}
The cache uses indexed slots rather than a Map because:
- Array access is faster
- The compiler knows exact slot count at build time
- No key hashing overhead
What The Compiler Can Memoize
Automatically Handled
function Component({ user, items, theme }) {
// ✅ Object literals
const style = { color: theme.primary, fontSize: 14 };
// ✅ Array literals
const options = [theme.optionA, theme.optionB];
// ✅ Arrow functions
const handleClick = () => doSomething(user.id);
// ✅ Function expressions
const process = function(item) { return transform(item); };
// ✅ JSX elements
const header = <Header user={user} />;
// ✅ Computed values
const total = items.reduce((sum, i) => sum + i.price, 0);
// ✅ Conditional expressions
const display = user.isAdmin ? <AdminPanel /> : <UserPanel />;
// ✅ Template literals
const greeting = `Hello, ${user.name}!`;
// ✅ Spread operations
const merged = { ...defaults, ...user.preferences };
// ✅ Method calls with stable receivers
const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
return (/* ... */);
}
Complex Control Flow
function Component({ items, filter, sortOrder }) {
// ✅ The compiler handles complex control flow
let result;
if (filter === 'active') {
result = items.filter(i => i.active);
} else if (filter === 'completed') {
result = items.filter(i => i.completed);
} else {
result = items;
}
if (sortOrder === 'asc') {
result = [...result].sort((a, b) => a.date - b.date);
} else {
result = [...result].sort((a, b) => b.date - a.date);
}
// The compiler tracks that `result` depends on items, filter, and sortOrder
// It generates appropriate cache invalidation logic
return <List items={result} />;
}
What The Compiler Cannot Handle
External Mutations
// ❌ BAD: Mutating external state
let globalCounter = 0;
function Component() {
globalCounter++; // Side effect — compiler can't track this
return <div>{globalCounter}</div>;
}
// The compiler may memoize the JSX, but the mutation
// means different renders should show different values.
// This breaks referential equality assumptions.
Object/Array Mutation
// ❌ BAD: Mutating objects
function Component({ items }) {
const sorted = items.sort(); // MUTATES items!
// Compiler assumes items is immutable
// Memoization will be incorrect
return <List items={sorted} />;
}
// ✅ GOOD: Create new reference
function Component({ items }) {
const sorted = [...items].sort(); // New array
return <List items={sorted} />;
}
Ref Mutations
// ⚠️ TRICKY: Ref mutations during render
function Component({ value }) {
const ref = useRef(null);
// ❌ BAD: Mutating ref during render
ref.current = value;
// The compiler doesn't track .current mutations
// It may cache values that should see new ref.current
return <div>{ref.current}</div>;
}
// ✅ GOOD: Mutate refs in effects or handlers
function Component({ value }) {
const ref = useRef(null);
useEffect(() => {
ref.current = value; // Safe — not during render
}, [value]);
return <div ref={ref} />;
}
Non-Deterministic Functions
// ❌ BAD: Non-deterministic during render
function Component() {
const id = Math.random(); // Different every render!
const time = Date.now(); // Different every render!
// Compiler may cache these, causing stale values
return <div id={id}>{time}</div>;
}
// ✅ GOOD: Use proper React patterns
function Component() {
const [id] = useState(() => Math.random()); // Stable
const [time, setTime] = useState(Date.now());
useEffect(() => {
const interval = setInterval(() => setTime(Date.now()), 1000);
return () => clearInterval(interval);
}, []);
return <div id={id}>{time}</div>;
}
Dynamic Property Access
// ⚠️ TRICKY: Dynamic property access
function Component({ data, key }) {
const value = data[key]; // Compiler may not track properly
// If `data` is the same reference but data[key] changed,
// compiler might use stale cached value
return <div>{value}</div>;
}
// The compiler IS getting better at this, but complex
// dynamic access patterns may still cause issues
The Rules of React (Enforced by Compiler)
The compiler doesn't just optimize — it enforces React's rules:
┌─────────────────────────────────────────────────────────────────────────────┐
│ RULES THE COMPILER ENFORCES │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ RULE 1: Components must be pure │
│ ───────────────────────────────── │
│ • Same inputs → same outputs │
│ • No mutations of props │
│ • No external side effects during render │
│ │
│ RULE 2: Hooks must be called unconditionally │
│ ───────────────────────────────────────────── │
│ • Same hooks in same order every render │
│ • No hooks inside conditions or loops │
│ • No early returns before hooks │
│ │
│ RULE 3: Values must be immutable │
│ ───────────────────────────────── │
│ • Never mutate state directly │
│ • Never mutate props │
│ • Treat all values from React as read-only │
│ │
│ RULE 4: Side effects belong in handlers/effects │
│ ──────────────────────────────────────────────── │
│ • No network requests during render │
│ • No DOM mutations during render │
│ • No logging during render (with compile-time exceptions) │
│ │
│ If your code violates these rules, the compiler either: │
│ 1. Refuses to compile the component (strict mode) │
│ 2. Skips optimization for that component (lenient mode) │
│ 3. Produces subtly broken memoization (if it can't detect) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
ESLint Plugin Integration
The compiler comes with an ESLint plugin that catches violations:
// eslint-plugin-react-compiler catches these:
// ❌ ERROR: Mutating props
function Component({ items }) {
items.push(newItem); // Cannot mutate props
}
// ❌ ERROR: Mutating state
function Component() {
const [state, setState] = useState({ count: 0 });
state.count++; // Cannot mutate state
setState(state);
}
// ❌ ERROR: Conditional hook call
function Component({ condition }) {
if (condition) {
useEffect(() => {}); // Hooks must be unconditional
}
}
// ❌ ERROR: Side effect during render
function Component() {
fetch('/api/data'); // Side effects must be in effects
return <div />;
}
Component Bailout Behavior
When the compiler can't optimize safely, it bails out:
// Compiler output when bailing out
function Component(props) {
'use no memo'; // Compiler adds this directive
// ... original code unchanged ...
}
// Or partial bailout — some parts optimized, some not
function Component({ data }) {
const $ = useMemoCache(2);
// ✅ This part is optimized
let header;
if ($[0] !== data.title) {
header = <Header title={data.title} />;
$[0] = data.title;
$[1] = header;
} else {
header = $[1];
}
// ⚠️ This part bailed out due to mutation detection
data.items.forEach(item => {
item.processed = true; // Mutation detected!
});
const items = data.items.map(i => <Item item={i} />);
return (
<div>
{header}
{items} {/* Not memoized due to mutation */}
</div>
);
}
Opt-Out Directives
// Opt out entire component
function Component() {
'use no memo';
// Compiler skips this component entirely
return <div />;
}
// Opt out specific code block (proposed)
function Component({ data }) {
const processed = (() => {
'use no memo';
// Complex logic the compiler shouldn't touch
return processData(data);
})();
return <Display data={processed} />;
}
Interaction with Existing Code
Existing useMemo/useCallback
// YOUR CODE (with manual memoization)
function Component({ items }) {
const sorted = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
const handleSelect = useCallback(
(id) => selectItem(id),
[]
);
return <List items={sorted} onSelect={handleSelect} />;
}
// COMPILER OUTPUT
function Component({ items }) {
const $ = useMemoCache(4);
// Compiler recognizes useMemo and optimizes it
let sorted;
if ($[0] !== items) {
sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
$[0] = items;
$[1] = sorted;
} else {
sorted = $[1];
}
// Compiler recognizes useCallback — may simplify deps
let handleSelect;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
handleSelect = (id) => selectItem(id);
$[2] = handleSelect;
} else {
handleSelect = $[2];
}
// JSX also memoized
let list;
if ($[1] !== sorted || $[2] !== handleSelect) {
list = <List items={sorted} onSelect={handleSelect} />;
$[3] = list;
} else {
list = $[3];
}
return list;
}
Key point: The compiler doesn't remove your useMemo/useCallback calls. It transforms them into the same cache system. Your existing code continues to work.
React.memo Components
// Child component with React.memo
const ExpensiveChild = React.memo(function ExpensiveChild({ data }) {
return <div>{/* expensive render */}</div>;
});
// Parent component
function Parent({ items }) {
const data = items.filter(i => i.active);
return <ExpensiveChild data={data} />;
}
// WITH COMPILER:
// - Parent's `data` is memoized (new reference only when items change)
// - ExpensiveChild's memo() wrapper checks reference equality
// - Combined effect: ExpensiveChild only re-renders when items actually change
// The compiler makes React.memo() MORE effective, not redundant
// React.memo stays useful for preventing re-renders from parent re-renders
// Compiler ensures props maintain stable references
Performance Characteristics
Cache Memory Overhead
┌─────────────────────────────────────────────────────────────────────────────┐
│ MEMORY OVERHEAD ANALYSIS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Each component instance gets a cache array: │
│ │
│ Cache slot = 8 bytes (64-bit pointer) │
│ Typical component = 5-20 cache slots │
│ Per-instance overhead = 40-160 bytes │
│ │
│ Example app: │
│ • 1,000 mounted component instances │
│ • Average 10 cache slots each │
│ • Total cache overhead: ~80 KB │
│ │
│ This is MUCH less than the memory saved by not recreating: │
│ • Object literals │
│ • Function closures │
│ • JSX element trees │
│ │
│ Net effect: Memory usage typically DECREASES with compiler │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Compile-Time Cost
┌─────────────────────────────────────────────────────────────────────────────┐
│ BUILD TIME IMPACT │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Compiler operations per component: │
│ 1. Parse AST (already done by Babel) │
│ 2. Build SSA form ~1-5ms per component │
│ 3. Analyze dependencies ~1-3ms per component │
│ 4. Generate memoization ~1-2ms per component │
│ 5. Emit transformed code (minimal) │
│ │
│ Typical impact: │
│ • Small app (100 components): +2-5 seconds build time │
│ • Medium app (500 components): +10-20 seconds build time │
│ • Large app (2000 components): +30-60 seconds build time │
│ │
│ The compiler is designed to be incremental: │
│ • Only re-compiles changed files │
│ • Caches analysis results │
│ • Hot reload remains fast (single file) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Runtime Performance
// Cache lookup is extremely fast
// Essentially: array[index] !== newValue
// Before (manual useMemo):
const memoized = useMemo(() => compute(a, b), [a, b]);
// Runtime: deps array allocation, comparison loop, potential compute
// After (compiler):
if ($[0] !== a || $[1] !== b) {
$[2] = compute(a, b);
$[0] = a;
$[1] = b;
}
const memoized = $[2];
// Runtime: 2 comparisons, no allocation
// Compiler version is typically FASTER than manual useMemo
// because it avoids deps array allocation
Writing Compiler-Friendly Code
Do: Keep Renders Pure
// ✅ Pure component — compiler loves this
function ProductCard({ product, onSelect }) {
const formattedPrice = formatCurrency(product.price);
const discountLabel = product.discount > 0
? `${product.discount}% off`
: null;
return (
<Card onClick={() => onSelect(product.id)}>
<Image src={product.image} />
<Title>{product.name}</Title>
<Price>{formattedPrice}</Price>
{discountLabel && <Badge>{discountLabel}</Badge>}
</Card>
);
}
Do: Use Immutable Updates
// ✅ Immutable state updates
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
setTodos(prev => [...prev, { id: Date.now(), text, done: false }]);
};
const toggleTodo = (id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
};
// Compiler can safely memoize all computations
const pendingCount = todos.filter(t => !t.done).length;
const doneCount = todos.filter(t => t.done).length;
return (/* ... */);
}
Do: Extract Static Values
// ❌ Creates new object every render (still memoized, but extra work)
function Component() {
const config = { timeout: 5000, retries: 3 };
return <Fetcher config={config} />;
}
// ✅ Better: static value outside component
const CONFIG = { timeout: 5000, retries: 3 };
function Component() {
return <Fetcher config={CONFIG} />;
}
// The compiler handles both, but the second is cleaner
// and has zero runtime overhead
Don't: Mutate Anything
// ❌ Mutation breaks memoization assumptions
function Component({ data }) {
data.processed = true; // NEVER DO THIS
const items = data.items;
items.sort(); // MUTATION!
items.reverse(); // MUTATION!
return <List items={items} />;
}
// ✅ Create new references
function Component({ data }) {
const processedData = { ...data, processed: true };
const items = [...data.items].sort().reverse();
return <List items={items} />;
}
Don't: Side Effects During Render
// ❌ Side effects during render
function Component({ userId }) {
console.log('Rendering for', userId); // Side effect
trackEvent('component_render', { userId }); // Side effect
localStorage.setItem('lastUser', userId); // Side effect
return <div>{userId}</div>;
}
// ✅ Side effects in proper places
function Component({ userId }) {
useEffect(() => {
console.log('Rendered for', userId);
trackEvent('component_render', { userId });
localStorage.setItem('lastUser', userId);
}, [userId]);
return <div>{userId}</div>;
}
Migration Guide
Step 1: Enable ESLint Plugin First
npm install eslint-plugin-react-compiler
// eslint.config.js
module.exports = {
plugins: ['react-compiler'],
rules: {
'react-compiler/react-compiler': 'error',
},
};
Fix all errors before enabling the compiler. The ESLint plugin catches most issues.
Step 2: Add Compiler to Build
npm install babel-plugin-react-compiler
// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
// Start lenient
compilationMode: 'infer', // or 'annotation' for explicit opt-in
panicThreshold: 'none', // Don't fail build on issues
}],
],
};
Step 3: Incremental Adoption
// Opt-in specific components first
function NewComponent() {
'use memo'; // Explicitly enable
return <div />;
}
// Or opt-out problematic ones
function LegacyComponent() {
'use no memo'; // Explicitly disable
// ... complex legacy code ...
}
Step 4: Monitor and Iterate
// React DevTools shows compiler info
// - Which components are compiled
// - Cache hit rates
// - Bailout reasons
// Look for:
// - Components that bail out frequently
// - Components with many cache slots (may indicate complexity)
// - Components that never cache hit (dependencies too granular)
Common Questions
Does it replace React.memo()?
No. React.memo() prevents re-renders when parent re-renders with same props. The compiler ensures props maintain stable references. They're complementary:
// React.memo: "Don't re-render if props haven't changed"
// Compiler: "Make sure props actually maintain reference equality"
const Child = React.memo(function Child({ data }) {
return <div>{data.value}</div>;
});
function Parent({ items }) {
// Without compiler: `filtered` is new reference every render
// With compiler: `filtered` is memoized
const filtered = items.filter(i => i.active);
// Now React.memo actually works because `filtered` is stable
return <Child data={filtered} />;
}
Should I remove useMemo/useCallback?
Not immediately. The compiler handles them fine. Over time, you can remove them for cleaner code, but it's not required.
What about Server Components?
Server Components don't need memoization — they run once per request. The compiler focuses on Client Components.
What about concurrent features?
The compiler is designed to work with concurrent React (Suspense, transitions, etc.). Its memoization doesn't break concurrent semantics.
Summary: The Mental Model Shift
┌─────────────────────────────────────────────────────────────────────────────┐
│ BEFORE vs AFTER COMPILER │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ BEFORE: │
│ ─────── │
│ "I need to think about memoization constantly" │
│ "Which values need useMemo? Which functions need useCallback?" │
│ "Did I get the dependency array right?" │
│ "Is this over-memoizing? Under-memoizing?" │
│ │
│ AFTER: │
│ ────── │
│ "I write plain JavaScript, compiler handles optimization" │
│ "I focus on correctness, not performance tricks" │
│ "I follow React's rules, compiler enforces them" │
│ "Memoization is automatic and optimal" │
│ │
│ THE KEY INSIGHT: │
│ ──────────────── │
│ The compiler doesn't just add memoization. │
│ It makes React's mental model simpler. │
│ │
│ You no longer need to think about "render performance." │
│ You think about "is my component correct and pure?" │
│ If yes → compiler makes it fast. │
│ If no → compiler tells you (via ESLint). │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
The React Compiler isn't just an optimization. It's a fundamental shift in how we write React components. The burden of memoization moves from runtime decisions (useMemo, useCallback) to compile-time analysis. You write simpler code. The compiler makes it fast.
The future of React is writing plain functions that happen to be optimal.
The best memoization is the memoization you don't have to think about. The React Compiler makes that possible.
What did you think?