React Compiler & Auto-Memoization: A Practical Guide
A hands-on guide to React Compiler and automatic memoization. Learn what it is, why it exists, how it replaces manual useMemo/useCallback/React.memo, and when you still need manual optimization.
For years, React developers have manually sprinkled useMemo, useCallback, and React.memo throughout their codebases to prevent unnecessary re-renders. It works — but it's tedious, error-prone, and clutters otherwise clean code. React Compiler (previously nicknamed 'React Forget') changes that entirely. It analyzes your components at build time and automatically inserts the right optimizations, so you don't have to think about them. This guide skips the basics and goes straight to practical examples so you can start using it today.
What Is React Compiler?
React Compiler is a build-time tool — a Babel/SWC plugin — that statically analyzes your React component code and transforms it into an optimized version before it ever reaches the browser. It understands React's rules (pure render functions, stable references, the Rules of Hooks) and uses that knowledge to automatically cache values and functions that would otherwise be recreated on every render.
Think of it as a smart compiler pass that reads your code the way an experienced React developer would, identifies what can safely be memoized, and rewrites the output accordingly. You write plain, readable React. The compiler produces the fast version.
React Compiler is not a runtime library. It runs once during your build and emits optimized JavaScript — there is zero overhead added to your bundle at runtime beyond what the optimization itself produces.
React Compiler shipped as a stable release alongside React 19. It is opt-in and fully backward-compatible — you can enable it incrementally on a file-by-file or directory basis.
Why React Compiler Was Introduced
Manual memoization has three well-known problems that the React team wanted to solve at the framework level:
- It's easy to get wrong. A missing dependency in a useMemo array, or wrapping the wrong function in useCallback, silently breaks correctness or provides no performance benefit.
- It adds noise. Every useMemo and useCallback wrapping a value is boilerplate that obscures the actual business logic.
- It doesn't scale. As components grow, developers must constantly audit memoization — a cognitive tax that compounds over time.
The React team's answer was to move memoization out of the developer's hands entirely. If the compiler can prove a value is stable across renders, it memoizes it. If it can't prove that, it leaves it alone — which is always the safe choice. This means the compiler never introduces bugs; it only adds optimizations it can guarantee are correct.
How React Compiler Automatically Optimizes Components
The compiler performs a static analysis called 'value tracking'. It traces every value in your component — props, state, derived values, callbacks — and determines whether that value can change between renders. If a value's inputs haven't changed, the compiler wraps it in an internal memoization call. This is equivalent to what you'd write manually with useMemo or useCallback, but it's generated for you.
Here's a concrete before/after to make this tangible. Imagine a component that filters a large list and passes a callback to a child:
// BEFORE — what you write today (manual memoization)
import { useMemo, useCallback, memo } from 'react';
const ProductList = memo(function ProductList({ products, onSelect }) {
return (
<ul>
{products.map(p => (
<li key={p.id} onClick={() => onSelect(p.id)}>{p.name}</li>
))}
</ul>
);
});
function Shop({ rawProducts, categoryId, onSelect }) {
const filtered = useMemo(
() => rawProducts.filter(p => p.categoryId === categoryId),
[rawProducts, categoryId]
);
const handleSelect = useCallback(
(id) => onSelect(id),
[onSelect]
);
return <ProductList products={filtered} onSelect={handleSelect} />;
}// AFTER — what you write with React Compiler (no manual memoization needed)
function ProductList({ products, onSelect }) {
return (
<ul>
{products.map(p => (
<li key={p.id} onClick={() => onSelect(p.id)}>{p.name}</li>
))}
</ul>
);
}
function Shop({ rawProducts, categoryId, onSelect }) {
const filtered = rawProducts.filter(p => p.categoryId === categoryId);
const handleSelect = (id) => onSelect(id);
return <ProductList products={filtered} onSelect={handleSelect} />;
}
// The compiler automatically memoizes `filtered`, `handleSelect`,
// and the ProductList component's output — no memo(), useMemo, or useCallback required.The 'After' version is shorter, easier to read, and just as fast. The compiler's output (what actually runs in the browser) looks closer to the 'Before' version — but you never have to write or maintain it.
React Compiler vs useMemo, useCallback, and React.memo
It helps to understand exactly what the compiler replaces and what it doesn't touch. Here's a direct comparison:
| Tool | Who writes it? | When does it run? | Risk of mistakes? |
| useMemo | Developer | Runtime (every render check) | Yes — stale deps, over/under memoization |
| useCallback | Developer | Runtime (every render check) | Yes — same as useMemo |
| React.memo | Developer | Runtime (shallow prop compare) | Yes — misses deep changes, adds wrapper noise |
| React Compiler | Compiler (auto-generated) | Build time (once) | No — only memoizes what it can prove is safe |
The key insight: useMemo, useCallback, and React.memo are runtime mechanisms — they add comparison logic that runs on every render. React Compiler is a build-time mechanism — it decides once, during compilation, what to memoize, and emits code that does exactly that. There's no repeated dependency-array diffing at runtime for values the compiler handles.
When React Compiler is enabled, you can safely remove most of your existing useMemo, useCallback, and React.memo calls. The compiler will handle them. The React team provides an ESLint rule and a codemod to help with this migration.
When Manual Memoization Is Still Useful
React Compiler is powerful, but it has deliberate limits. It only memoizes what it can statically prove is safe. There are real scenarios where you still reach for manual tools:
- Expensive computations with external/non-React inputs: If a value depends on a Date.now() call, a random number, or a mutable external object, the compiler cannot prove stability and will skip memoization. You may still want useMemo here.
- Interop with non-React libraries: Third-party libraries that use referential equality checks (e.g., charting libraries, virtualization libraries) may need you to guarantee a stable reference manually.
- Custom hooks with complex dependency graphs: If your hook's output depends on values the compiler can't trace (e.g., values from a ref that mutates), manual memoization remains the right tool.
- Intentional cache invalidation control: Sometimes you want fine-grained control over when a cache busts. useMemo with an explicit key-like dependency gives you that control explicitly.
- Code that violates Rules of Hooks or React's purity rules: The compiler skips files/components it detects as non-compliant. Fix the violations first, or memoize manually in the interim.
A practical rule of thumb: write clean, plain React first. Let the compiler do its job. Only reach for useMemo or useCallback when you have a measured performance problem the compiler didn't solve, or when you're working with external systems that need guaranteed stable references.
How to Enable React Compiler in Your Project
React Compiler requires React 19 (or the React 18 compatibility shim). Here's how to enable it for the most common setups:
# Step 1: Install the compiler package
npm install --save-dev babel-plugin-react-compiler
# If you're using React 18 instead of 19, also install the runtime shim:
npm install react-compiler-runtime// Step 2a: babel.config.json (for Create React App, Expo, or plain Babel setups)
{
"plugins": [
["babel-plugin-react-compiler", {
"target": "18" // omit or set to "19" if using React 19
}]
]
}// Step 2b: next.config.js (for Next.js 15+)
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
reactCompiler: true,
},
};
module.exports = nextConfig;// Step 2c: vite.config.js (using vite-plugin-babel)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
['babel-plugin-react-compiler', { target: '19' }],
],
},
}),
],
});After enabling the compiler, run your app and open React DevTools. Components optimized by the compiler will show a 'Memo ✨' badge in the component tree — a quick visual confirmation that the compiler is working.
To enable the compiler incrementally (recommended for large codebases), use the compilationMode option to target specific directories or add a 'use memo' directive at the top of individual files:
// Opt a single file into React Compiler explicitly
'use memo';
import React from 'react';
export function MyComponent({ value }) {
const doubled = value * 2; // compiler will memoize this
return <div>{doubled}</div>;
}Common Mistakes to Avoid With React Compiler
Even with a compiler doing the heavy lifting, there are pitfalls that will prevent it from optimizing your code — or worse, cause it to skip your components entirely.
- Mutating props or state directly: React Compiler assumes your components are pure. If you mutate an array or object in place (e.g., arr.push(item) instead of [...arr, item]), the compiler cannot safely memoize values derived from it and will bail out.
- Breaking the Rules of Hooks: Calling hooks conditionally or inside loops causes the compiler to skip the entire component. Fix hook violations before expecting compiler optimizations.
- Relying on object identity for side effects: If your useEffect depends on an object reference changing to trigger, the compiler memoizing that object will silently break your effect. Use primitive values or explicit flags as effect dependencies instead.
- Keeping dead useMemo/useCallback calls: After enabling the compiler, leftover manual memoization calls don't cause bugs, but they do add unnecessary runtime overhead. Run the provided codemod to clean them up.
- Not running the React Compiler health check: Before enabling globally, run npx react-compiler-healthcheck in your project root. It scans your codebase and reports components that violate React's rules and will be skipped by the compiler.
# Run the health check before enabling the compiler globally
npx react-compiler-healthcheck
# Example output:
# Successfully compiled: 143 components
# Skipped (violations found): 7 components
# See details: react-compiler-healthcheck-report.jsonFrequently Asked Questions
- Does React Compiler replace React.memo entirely? For most cases, yes. The compiler memoizes component output automatically. You only need React.memo if you're working with a library or pattern the compiler can't analyze.
- Will it break my existing code? No. The compiler only applies optimizations it can prove are safe. If it's uncertain, it skips that component. Your app will still work — it just won't be optimized in that spot.
- Does it work with TypeScript? Yes. The compiler works on both JavaScript and TypeScript files. Use babel-plugin-react-compiler with @babel/preset-typescript, or use the SWC plugin if your toolchain supports it.
- Can I use it with React 18? Yes, with the react-compiler-runtime shim. Set the target option to '18' in the plugin config. Full support is in React 19.
- Does it increase my bundle size? Slightly. The compiler adds memoization wrappers to the emitted code. In practice, the savings from fewer re-renders far outweigh the small increase in bundle size.
- Should I remove all my useMemo and useCallback calls immediately? Not all at once. Enable the compiler, run your test suite, then use the codemod (npx codemod react/19/replace-use-memo-with-compiler) to remove redundant calls incrementally.
- Does React Compiler work with Server Components? The compiler optimizes Client Components. React Server Components don't re-render on the client, so memoization doesn't apply to them.
Next Steps and Further Reading
React Compiler is one of the most impactful additions to the React ecosystem in years. It doesn't change how you write React — it just makes what you already write faster, automatically. Here's a suggested path forward:
- Run npx react-compiler-healthcheck on your existing project to see how many components are compiler-ready today.
- Enable the compiler in a non-critical part of your app (a single route or feature directory) and verify behavior with your test suite.
- Use React DevTools to confirm the 'Memo ✨' badges are appearing on your components.
- Run the codemod to remove redundant useMemo, useCallback, and React.memo calls from compiler-enabled files.
- Gradually expand compiler coverage across your codebase, fixing any Rules of Hooks violations you discover along the way.
The goal is a codebase where you write straightforward React — no performance boilerplate — and the compiler handles the rest. That future is available today. Start small, measure, and expand with confidence.