The End of Manual Memoization
For years, React developers have relied on useMemo, useCallback, and React.memo to prevent unnecessary re-renders. These APIs work, but they add cognitive overhead, clutter codebases, and are easy to misuse. The React Compiler changes everything.
Shipping with React 19, the React Compiler (formerly "React Forget") is an ahead-of-time build tool that automatically memoizes components and hooks at compile time. It understands the rules of React, tracks data dependencies, and inserts fine-grained memoization where it's actually needed — without you writing a single memo wrapper.
- Automatic memoization at build time — no runtime overhead from useMemo/useCallback
- Fine-grained dependency tracking — more precise than manual dependency arrays
- Zero code changes required — works with existing idiomatic React code
- Eliminates entire categories of performance bugs (stale closures, missing deps)
- Ships as a Babel plugin for Next.js 16 with first-class integration
#1 How the React Compiler Works
Understanding the compiler's mental model is essential before adopting it. It doesn't just wrap everything in useMemo — it performs static analysis to understand your component's data flow and only memoizes what matters.
Compilation Pipeline
The compiler operates during build time through three phases:
- Parse: Analyzes your component's source code into an intermediate representation (IR)
- Analyze: Tracks how every value flows through the component — which variables are read, which are mutated, and which are stable across renders
- Transform: Inserts memoization boundaries around values that would cause unnecessary re-computation or re-rendering
// What you write — clean, no manual memoization
function ProductCard({ product, onAddToCart }) {
const discount = product.price * 0.1;
const finalPrice = product.price - discount;
const formattedPrice = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(finalPrice);
return (
<div className="card">
<h3>{product.name}</h3>
<span>{formattedPrice}</span>
<button onClick={() => onAddToCart(product.id)}>
Add to Cart
</button>
</div>
);
}// What the compiler produces — fine-grained caching
function ProductCard({ product, onAddToCart }) {
const $ = _c(5); // compiler-managed cache slots
let formattedPrice;
if ($[0] !== product.price) {
const discount = product.price * 0.1;
const finalPrice = product.price - discount;
formattedPrice = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(finalPrice);
$[0] = product.price;
$[1] = formattedPrice;
} else {
formattedPrice = $[1];
}
let t0;
if ($[2] !== product.name || $[3] !== formattedPrice || $[4] !== onAddToCart) {
t0 = (
<div className="card">
<h3>{product.name}</h3>
<span>{formattedPrice}</span>
<button onClick={() => onAddToCart(product.id)}>
Add to Cart
</button>
</div>
);
$[2] = product.name;
$[3] = formattedPrice;
$[4] = onAddToCart;
}
return t0;
}Key Insight: Granularity
Notice how the compiler doesn't just memoize the entire component. It creates separate cache slots for the price calculation and the JSX output. This is more granular than what most developers would do manually — and it's why the compiler often outperforms hand-optimized code.
- Manual useMemo typically wraps large blocks — the compiler memoizes individual expressions
- The compiler tracks exact dependencies — no risk of stale closures from missing deps
- It uses a flat cache array (_c) with positional slots — minimal memory overhead
- More precise memoization than manual optimization
- Zero developer effort
- No risk of dependency array mistakes
#2 Enabling the Compiler in Next.js 16
Next.js 16 ships with first-class React Compiler support. Enabling it is a single configuration change, but understanding the options helps you adopt incrementally.
Basic Setup
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
reactCompiler: true,
},
};
export default nextConfig;That's it. The compiler will now analyze and optimize every component and hook in your application during build.
Incremental Adoption with Opt-In Mode
For large codebases, you can enable the compiler in opt-in mode — only components explicitly marked with "use memo" will be compiled:
const nextConfig: NextConfig = {
experimental: {
reactCompiler: {
compilationMode: 'annotation',
},
},
};'use memo';
// Only this file is compiled in annotation mode
export function ExpensiveList({ items, filter }) {
const filtered = items.filter(item =>
item.name.includes(filter)
);
return (
<ul>
{filtered.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}Opting Out Specific Components
If a component doesn't follow the rules of React (e.g., mutates during render), you can opt it out with "use no memo":
'use no memo';
// This component is skipped by the compiler
export function LegacyChart({ data }) {
// ... uses patterns incompatible with compiler
}- One-line setup in Next.js 16
- Incremental adoption for large codebases
- Escape hatch for edge cases
#3 What You Can Remove
The biggest practical benefit of the React Compiler is the code you can delete. Manual memoization primitives are no longer needed in compiled components — and removing them makes your code cleaner and easier to maintain.
Before: Manual Memoization
'use client';
import { memo, useCallback, useMemo } from 'react';
interface DashboardProps {
data: DataPoint[];
onExport: (format: string) => void;
}
// Wrapped in React.memo to prevent re-renders
const MetricCard = memo(function MetricCard({
label,
value,
}: {
label: string;
value: number;
}) {
return (
<div className="rounded-lg border p-4">
<span className="text-sm text-gray-500">{label}</span>
<span className="text-2xl font-bold">{value}</span>
</div>
);
});
export function Dashboard({ data, onExport }: DashboardProps) {
// Manual useMemo for derived data
const totals = useMemo(() => ({
revenue: data.reduce((sum, d) => sum + d.revenue, 0),
users: data.reduce((sum, d) => sum + d.users, 0),
orders: data.reduce((sum, d) => sum + d.orders, 0),
}), [data]);
// Manual useCallback to stabilize reference
const handleExport = useCallback((format: string) => {
onExport(format);
}, [onExport]);
// Manual useMemo for formatted values
const formattedRevenue = useMemo(
() => new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(totals.revenue),
[totals.revenue]
);
return (
<div>
<MetricCard label="Revenue" value={totals.revenue} />
<MetricCard label="Users" value={totals.users} />
<span>{formattedRevenue}</span>
<button onClick={() => handleExport('csv')}>Export</button>
</div>
);
}After: Compiler-Optimized
'use client';
interface DashboardProps {
data: DataPoint[];
onExport: (format: string) => void;
}
// No React.memo needed — compiler handles it
function MetricCard({
label,
value,
}: {
label: string;
value: number;
}) {
return (
<div className="rounded-lg border p-4">
<span className="text-sm text-gray-500">{label}</span>
<span className="text-2xl font-bold">{value}</span>
</div>
);
}
export function Dashboard({ data, onExport }: DashboardProps) {
// Plain computation — compiler memoizes automatically
const totals = {
revenue: data.reduce((sum, d) => sum + d.revenue, 0),
users: data.reduce((sum, d) => sum + d.users, 0),
orders: data.reduce((sum, d) => sum + d.orders, 0),
};
// Plain function — compiler stabilizes the reference
const handleExport = (format: string) => {
onExport(format);
};
// Plain formatting — compiler caches based on input
const formattedRevenue = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(totals.revenue);
return (
<div>
<MetricCard label="Revenue" value={totals.revenue} />
<MetricCard label="Users" value={totals.users} />
<span>{formattedRevenue}</span>
<button onClick={() => handleExport('csv')}>Export</button>
</div>
);
}What Can Be Safely Removed
- useMemo for derived data — the compiler tracks input dependencies automatically
- useCallback for event handlers — reference stability is handled at compile time
- React.memo on child components — the compiler prevents unnecessary re-renders
- Complex dependency arrays — no more missed dependencies or stale closures
Important: Removing manual memoization is safe only when the compiler is enabled. Keep them if you need to support non-compiled builds.
- Cleaner, more readable components
- Fewer lines of code to maintain
- No more dependency array bugs
#4 The Rules of React the Compiler Enforces
The React Compiler relies on your code following the 'Rules of React.' If your components break these rules, the compiler may skip them or produce incorrect output. Understanding these rules is now more important than ever.
Rule 1: No Mutation During Render
function SortedList({ items }) {
// ❌ Mutates the prop directly during render
items.sort((a, b) => a.name.localeCompare(b.name));
return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}function SortedList({ items }) {
// ✅ Creates a new sorted array — no mutation
const sorted = [...items].sort(
(a, b) => a.name.localeCompare(b.name)
);
return (
<ul>
{sorted.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}Rule 2: Stable Hook Call Order
function Profile({ userId }) {
// ❌ Hook called conditionally
if (!userId) return <p>No user</p>;
const user = useUser(userId);
return <p>{user.name}</p>;
}function Profile({ userId }) {
// ✅ Hook always called, guard after
const user = useUser(userId);
if (!userId || !user) return <p>No user</p>;
return <p>{user.name}</p>;
}Rule 3: No External Mutable State in Render
let renderCount = 0;
function Counter() {
// ❌ Reading and mutating external variable during render
renderCount++;
return <p>Rendered {renderCount} times</p>;
}function Counter() {
// ✅ useRef for mutable values that don't affect render
const renderCount = useRef(0);
useEffect(() => {
renderCount.current++;
});
return <p>Counter component</p>;
}Checking Compliance with ESLint
The React team provides an ESLint plugin that catches rule violations before you even run the compiler:
npm install -D eslint-plugin-react-compilerimport reactCompiler from 'eslint-plugin-react-compiler';
export default [
{
plugins: {
'react-compiler': reactCompiler,
},
rules: {
'react-compiler/react-compiler': 'error',
},
},
];- Catches violations before build
- Ensures compiler can optimize every component
- Enforces React best practices team-wide
#5 Performance Impact in Production
The compiler's impact isn't theoretical — it produces measurable improvements in real-world applications, particularly in components with frequent re-renders and complex derived state.
Where You See the Biggest Gains
- List components with expensive mapping/filtering — compiler caches derived arrays
- Form components with many fields — only changed fields trigger re-computation
- Dashboard layouts with many child components — compiler prevents prop-triggered cascading re-renders
- Components with complex Intl formatting — locale formatting is cached by input value
- Callback-heavy interactive UIs — function references are automatically stabilized
Measuring Compiler Impact
const nextConfig: NextConfig = {
experimental: {
reactCompiler: {
// Enable compilation logging in development
// to see which components are being compiled
panicThreshold: 'NONE',
},
},
};Use React DevTools Profiler to compare render counts and durations before and after enabling the compiler. Focus on:
- Total render count per interaction — should decrease significantly
- Render duration for complex components — should drop for unchanged inputs
- Committed renders vs skipped renders — more should be skipped
When the Compiler Won't Help
- Network-bound operations — fetch latency is not a rendering problem
- Components that legitimately need to re-render on every update (e.g., real-time tickers)
- First render performance — memoization only helps on subsequent renders
- Server Components — they don't re-render on the client, so memoization is irrelevant
- Measurable render count reduction
- Lower interaction-to-paint latency (INP)
- Reduced memory allocation from fewer re-computations
#6 Migration Strategy for Existing Codebases
Adopting the compiler in an existing project requires a systematic approach. Here's a proven migration path that minimizes risk.
Phase 1: Audit with ESLint
Install the ESLint plugin first and fix all violations before enabling the compiler:
# Install the plugin
npm install -D eslint-plugin-react-compiler
# Run across the codebase
npx eslint --ext .tsx,.ts src/
# Common violations to fix:
# - Mutating props or state during render
# - Conditional hook calls
# - Reading mutable globals in renderPhase 2: Enable in Annotation Mode
Start with opt-in mode and mark your most performance-critical components:
// next.config.ts
experimental: {
reactCompiler: {
compilationMode: 'annotation',
},
}
// Then add 'use memo' to specific files
// Start with:
// 1. Heavy list/table components
// 2. Form components with many fields
// 3. Dashboard/analytics componentsPhase 3: Enable Globally
Once confident, switch to full compilation and opt out specific files if needed:
// next.config.ts
experimental: {
reactCompiler: true,
}
// Opt out problematic files with 'use no memo'
// at the top of the file if neededPhase 4: Remove Manual Memoization
Gradually remove useMemo, useCallback, and React.memo from compiled components. The compiler handles these automatically, so manual memoization is now redundant (though harmless if left in place).
- Remove React.memo wrappers — the compiler prevents unnecessary child re-renders
- Remove useMemo for derived data — the compiler caches computations automatically
- Remove useCallback for handlers — reference stability is compiler-managed
- Keep useMemo for intentional semantic guarantees (e.g., stable object identity for context values)
- Risk-free incremental adoption
- No big-bang migration required
- Cleaner codebase after removing manual memoization
Common Misconceptions
- "The compiler replaces React.memo" — Partially true. It handles the same optimization automatically, but React.memo still works and isn't harmful to keep
- "I need to rewrite my code" — No. The compiler works with existing idiomatic React. Only rule violations need fixing
- "It makes every component faster" — Only components that were re-rendering unnecessarily. If your component legitimately needs to re-render, the compiler won't skip it
- "It eliminates the need to think about performance" — It eliminates memoization busywork, but architecture decisions (server vs client, data fetching strategy) still matter
- "useMemo and useCallback will be deprecated" — No. They remain valid APIs. The compiler simply makes them unnecessary in most cases
Compiler vs Manual: When Manual Still Wins
While the compiler handles most cases, there are scenarios where manual control is still valuable:
- Context values with object identity requirements — useMemo ensures stable references for context providers to prevent consumer re-renders
- Expensive initializers — useState(() => heavyCompute()) runs once; the compiler doesn't optimize initial state
- Web Worker communication — stable callback references for postMessage handlers may need explicit useCallback
- Third-party library integration — some libraries depend on referential equality for callbacks passed as props
Key Takeaways
- 1The React Compiler automatically memoizes components and hooks at build time — no code changes needed
- 2It's more granular than manual useMemo/useCallback — it tracks individual expression dependencies
- 3Enable in Next.js 16 with a single config line: experimental.reactCompiler = true
- 4Use annotation mode ('use memo' directive) for incremental adoption in large codebases
- 5Follow the Rules of React: no mutation during render, stable hook order, no mutable globals
- 6Install eslint-plugin-react-compiler to catch violations before they cause issues
- 7After enabling, you can safely remove useMemo, useCallback, and React.memo from compiled components
- 8The compiler doesn't replace architectural decisions — server/client boundaries and data strategy still matter
Final Thoughts
The React Compiler is the most impactful change to React's developer experience since hooks. It removes one of the most tedious aspects of React development — manually tracking dependencies and wrapping values in memoization primitives — and replaces it with automatic, compile-time optimization that's more precise than what most developers would write by hand.
For teams on Next.js 16, adoption is trivial. For those with legacy codebases, the incremental path (ESLint audit → annotation mode → full compilation → cleanup) makes migration safe and measurable.
The future of React performance isn't about writing more memoization code — it's about writing clean, idiomatic components and letting the compiler handle the rest.
- React Compiler = automatic useMemo + useCallback + React.memo at build time
- One config line enables it in Next.js 16
- Works with existing code — no rewrite needed
- Follow the Rules of React and use the ESLint plugin
- Adopt incrementally: ESLint → annotation mode → full → cleanup
- Write clean components, let the compiler optimize
