React Compiler and Automatic Memoization
Understand how the React Compiler works, what automatic memoization means, and how it eliminates the need for useMemo, useCallback, and React.memo.
What you'll learn
- ✓What the React Compiler does under the hood
- ✓How automatic memoization replaces manual optimization
- ✓The rules your code must follow
- ✓How to adopt the compiler incrementally
- ✓Debugging compiler output
Prerequisites
- •Comfortable with React components and hooks
- •Familiarity with useMemo and useCallback
The React Compiler (previously known as React Forget) is a build-time tool that analyzes your components and hooks, then automatically inserts memoization where it helps. Instead of you deciding where to place useMemo, useCallback, or React.memo, the compiler figures it out for you. The result is code that is both simpler to write and faster to run.
Why Manual Memoization Falls Short
Developers often memoize too little or too much. Missing a single dependency causes stale data. Adding useMemo around a cheap computation adds overhead without benefit. The cognitive load is real: every new value or callback forces you to ask whether it needs wrapping.
The compiler removes this burden by treating memoization as a compilation concern rather than an authoring concern.
How the Compiler Works
At build time, the compiler parses each component and hook function. It builds a dependency graph of every value, tracking which variables depend on which others. Then it groups values into memoization blocks. If a group of values depends only on inputs that have not changed since the last render, the compiler wraps them so React can skip recomputation.
Consider this component:
function ProductCard({ product, onAddToCart }) {
const discountedPrice = product.price * (1 - product.discount);
const formattedPrice = `$${discountedPrice.toFixed(2)}`;
const handleClick = () => {
onAddToCart(product.id, discountedPrice);
};
return (
<div>
<h3>{product.name}</h3>
<p>{formattedPrice}</p>
<button onClick={handleClick}>Add to Cart</button>
</div>
);
}
Without the compiler, discountedPrice, formattedPrice, and handleClick are recalculated on every render, even if product and onAddToCart have not changed. The compiler detects that all three depend only on those two props and wraps them in a cache block. The compiled output is conceptually similar to:
function ProductCard({ product, onAddToCart }) {
const $ = useMemoCache(4);
let discountedPrice, formattedPrice, handleClick;
if ($[0] !== product || $[1] !== onAddToCart) {
discountedPrice = product.price * (1 - product.discount);
formattedPrice = `$${discountedPrice.toFixed(2)}`;
handleClick = () => onAddToCart(product.id, discountedPrice);
$[0] = product;
$[1] = onAddToCart;
$[2] = formattedPrice;
$[3] = handleClick;
} else {
formattedPrice = $[2];
handleClick = $[3];
}
return (
<div>
<h3>{product.name}</h3>
<p>{formattedPrice}</p>
<button onClick={handleClick}>Add to Cart</button>
</div>
);
}
You never write this version. The compiler generates it from your clean, readable source code.
The Rules of React
The compiler relies on your code following the Rules of React. If you break them, the compiler either skips the component or produces incorrect output.
The key rules are:
- Components and hooks must be pure during render. No side effects, no mutations of external variables, no reading from mutable globals.
- Props and state are immutable. Never mutate them directly. Always return new objects from state updates.
- Hook calls must be unconditional. No hooks inside loops, conditions, or nested functions.
- Hook return values and arguments are immutable. Do not mutate the array returned by
useState, for example.
// Bad - mutates during render
function Counter({ items }) {
const sorted = items.sort(); // mutates the prop array
return <ul>{sorted.map(i => <li key={i}>{i}</li>)}</ul>;
}
// Good - creates a new array
function Counter({ items }) {
const sorted = [...items].sort();
return <ul>{sorted.map(i => <li key={i}>{i}</li>)}</ul>;
}
Setting Up the Compiler
The compiler ships as a Babel plugin. Install it and add it to your build configuration.
npm install -D babel-plugin-react-compiler
For a Vite project, add it to your Babel configuration:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: ['babel-plugin-react-compiler'],
},
}),
],
});
Next.js has built-in support. Enable it in next.config.js:
// next.config.js
module.exports = {
experimental: {
reactCompiler: true,
},
};
Incremental Adoption
You do not have to compile your entire codebase at once. The compiler supports an opt-in mode where only components and hooks with a specific directive are compiled.
// Only this component will be compiled
function Dashboard() {
'use memo';
const stats = computeStats(data);
return <StatsGrid stats={stats} />;
}
You can also use an opt-out directive for components that intentionally break the rules (legacy code, third-party integrations):
function LegacyWidget({ config }) {
'use no memo';
// This component mutates config internally
externalLib.init(config);
return <div ref={el => externalLib.mount(el)} />;
}
Debugging Compiler Output
The compiler provides an ESLint plugin that warns you when your code violates the Rules of React. Install it to catch issues early.
npm install -D eslint-plugin-react-compiler
// .eslintrc.js
module.exports = {
plugins: ['react-compiler'],
rules: {
'react-compiler/react-compiler': 'error',
},
};
When something behaves unexpectedly, you can inspect the compiled output. The compiler adds comments to the generated code showing which values were grouped into cache slots. In development mode, React DevTools marks compiled components with a badge.
What You Can Remove
Once the compiler is active and your components follow the rules, you can safely remove:
useMemocalls for computed valuesuseCallbackwrappers around event handlersReact.memowrappers around child components
The compiler handles all three automatically and often does a better job because it can memoize at a finer granularity than component-level React.memo.
// Before - manual memoization
const MemoizedChild = React.memo(function Child({ data, onClick }) {
const processed = useMemo(() => transform(data), [data]);
const handler = useCallback(() => onClick(processed), [onClick, processed]);
return <button onClick={handler}>{processed.label}</button>;
});
// After - let the compiler handle it
function Child({ data, onClick }) {
const processed = transform(data);
const handler = () => onClick(processed);
return <button onClick={handler}>{processed.label}</button>;
}
When to Wait
The compiler is production-ready but still evolving. Hold off if your codebase heavily relies on mutation patterns during render, uses class components extensively, or depends on libraries that mutate props. Run the ESLint plugin first to gauge how much of your code is already compliant.
For new projects, adopt the compiler from day one. For existing projects, enable it incrementally and let the linter guide your cleanup. Either way, the future of React performance is automatic.
Related articles
- React React Performance: Memo, useMemo, and useCallback Guide
Optimize React performance with React.memo, useMemo, and useCallback. Learn when to use each, how to profile, and avoid common pitfalls.
- React React useTransition: Advanced Patterns for Responsive UIs
Go beyond basic useTransition usage with patterns for search, navigation, tab switching, and priority-based rendering in React concurrent mode.
- React React Concurrent Features: useTransition, useDeferredValue, and More
Master React's concurrent rendering features including useTransition, useDeferredValue, and strategies for keeping your UI responsive.
- React React Context Performance: Splitting, Memoizing, and Selectors
Solve React Context performance problems with provider splitting, value memoization, selector patterns, and smart composition strategies.