automatic memoization explained
Manual memoization with useMemo and useCallback is error prone, hard to maintain and frequently used incorrectly. The React Compiler analyzes component code at build time and automatically inserts the right memoization wrappers, without developers having to manage dependency arrays or decide when something should be cached. How that works and what it means.
Table of Contents
- 1. The memoization problem in React before the compiler
- 2. What the React Compiler actually does
- 3. The Rules of React: what the compiler requires
- 4. Enabling React Compiler: Babel, SWC and Vite
- 5. eslint-plugin-react-compiler: finding violations
- 6. useMemo and useCallback: before and after
- 7. Limits of the React Compiler: what it cannot do
- 8. Introducing React Compiler into existing codebases
- 9. Manual memoization vs. React Compiler compared
- 10. Summary
- 11. FAQ
1. The memoization problem in React before the compiler
Manual memoization with useMemo and useCallback is one of the biggest sources of bugs and technical debt in React codebases. The problem has three dimensions. First, overuse is common: useMemo is applied to simple computations where the overhead of memoization outweighs the benefit. Second, underuse is common: expensive computations or objects passed as props that cause unnecessary re-renders in child components are not cached. Third, stale dependencies are a classic bug: the dependency array of useMemo or useCallback is incomplete, the cached value goes stale and the component behaves inconsistently.
The React Compiler, developed internally at Meta as "React Forget", solves all three dimensions at once. It analyzes the code statically and decides what needs to be cached, what does not and which dependencies are correct. Developers no longer need to maintain dependency arrays, no longer need to make decisions about memoization worthiness and no longer need to debug bugs caused by stale closures. The result is a codebase that is consistently performant, without manual optimization effort.
2. What the React Compiler actually does
The React Compiler is a build-time compiler, not a runtime system. It analyzes the AST (Abstract Syntax Tree) of React components at build time and transforms the code. It identifies values and functions that can stay stable between renders, because their inputs have not changed, and wraps these automatically in the equivalent memoization code. The generated JavaScript is equivalent to manual useMemo and useCallback, but more precise: the compiler sees the entire scope and can decide more granularly which sub-expressions can be cached.
It is important to understand: the React Compiler does not generate different React code, it optimizes the existing one. A component that works correctly without the compiler works exactly the same way with the compiler, just potentially with fewer re-renders. The compiler does not add new runtime behavior and does not change semantics. It is purely additive with regard to optimizations. That makes it safe to use in existing codebases, provided the code follows the Rules of React, which is the central requirement.
// BEFORE React Compiler: manual memoizing required
import { useMemo, useCallback, memo } from 'react';
const ProductCard = memo(function ProductCard({
product,
onAddToCart,
}: {
product: Product;
onAddToCart: (id: string) => void;
}) {
// Developer must manually decide: is this expensive enough for useMemo?
const discountedPrice = useMemo(
() => product.price * (1 - product.discountRate),
[product.price, product.discountRate]
);
// Developer must manually wrap callback to keep reference stable
const handleAddToCart = useCallback(
() => onAddToCart(product.id),
[onAddToCart, product.id]
);
return (
<div>
<h3>{product.name}</h3>
<p>{discountedPrice.toFixed(2)} €</p>
<button onClick={handleAddToCart}>In den Warenkorb</button>
</div>
);
});
// AFTER React Compiler: no useMemo, no useCallback, no memo() needed
// The compiler analyzes the component and adds memoizing automatically
function ProductCard({
product,
onAddToCart,
}: {
product: Product;
onAddToCart: (id: string) => void;
}) {
// Compiler sees: discountedPrice depends only on product.price and product.discountRate
// It automatically caches this, no manual useMemo needed
const discountedPrice = product.price * (1 - product.discountRate);
// Compiler sees: this function only depends on onAddToCart and product.id
// It wraps it in equivalent of useCallback automatically
const handleAddToCart = () => onAddToCart(product.id);
return (
<div>
<h3>{product.name}</h3>
<p>{discountedPrice.toFixed(2)} €</p>
<button onClick={handleAddToCart}>In den Warenkorb</button>
</div>
);
}
3. The Rules of React: what the compiler requires
The React Compiler requires that the code follows the "Rules of React", a set of invariants that define the React component model. The most important ones: components and hooks must produce the same outputs for the same inputs (referential transparency / idempotence). State and props must never be mutated directly, no props.list.push(item), no state.value = newValue. Hooks must not be called conditionally, no if (condition) { useState() }. Side effects in render functions are forbidden, everything belongs outside of useEffect.
Why these rules are so important for the React Compiler: the compiler builds its memoization model on the assumption that a function produces the same result for the same inputs. If a component mutates directly or makes global state changes inside the render function, the compiler can no longer safely decide when results can be cached. It then skips optimization for that component, or, for critical violations, emits a build-time warning. The ESLint plugin eslint-plugin-react-compiler checks the code for these violations before the compiler is enabled.
4. Enabling React Compiler: Babel, SWC and Vite
The React Compiler is installed as a build-time plugin: for Babel-based setups as babel-plugin-react-compiler, for SWC-based setups (Next.js 15+ uses SWC) directly via the Next.js configuration. In Vite projects using @vitejs/plugin-react, the compiler is passed as a Babel plugin in the Vite configuration. Installation is standardized and takes only a few lines of configuration. By default the compiler is opt-in for all components, with the option to exclude individual components using a pragma comment (// @disableReactCompiler).
The step-by-step introduction strategy for the React Compiler in existing projects: first install eslint-plugin-react-compiler and document all violations. Then fix them systematically, starting with the simplest cases (direct mutations, missing hook rules). After that, enable the compiler in "annotation only" mode, in which it only reports problems but does not yet insert optimizations. Only once all lint errors are fixed should the compiler be enabled in full mode. This process ensures that no existing functionality is changed by the compiler.
// babel.config.js, enabling React Compiler as Babel plugin
module.exports = {
plugins: [
// React Compiler must be listed BEFORE other JSX transforms
['babel-plugin-react-compiler', {
// Optional: only compile specific files during migration
// sources: (filename) => filename.indexOf('src/') !== -1,
// Target React version, ensures correct memoizing semantics
target: '19',
}],
'@babel/plugin-transform-react-jsx',
],
};
// next.config.js, enabling React Compiler in Next.js 15+
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
reactCompiler: true,
// Or with options:
// reactCompiler: {
// compilationMode: 'annotation', // only compile files with "use memo" pragma
// },
},
};
module.exports = nextConfig;
// vite.config.ts, Vite with React Compiler via Babel plugin
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
// Pass React Compiler as Babel plugin to the React Vite plugin
['babel-plugin-react-compiler', { target: '19' }],
],
},
}),
],
});
// Per-component opt-out: add pragma comment to exclude from compilation
// 'use no memo'; // at top of file or component
function LegacyComponent() {
// This component will NOT be optimized by React Compiler
return <div>Legacy code not yet compliant with Rules of React</div>;
}
5. eslint-plugin-react-compiler: finding violations
eslint-plugin-react-compiler is the recommended tool for checking code against the Rules of React before enabling the React Compiler. The plugin analyzes the same code as the compiler itself and reports exactly the places that the compiler cannot optimize or that would lead to incorrect behavior. The most common violations: direct mutation of state objects (state.items.push(x)), conditions before hook calls, use of variables from the outer scope in a way that violates referential transparency, and side effects in render functions outside of useEffect.
The plugin can also be run in "strict mode", which reports all potentially problematic patterns, not just the ones that directly block the React Compiler. That is suitable for new projects that want to write compiler-compatible code from the start. For existing projects, "warn" mode makes more sense, reporting only the actual compiler blockers as errors. The ESLint plugin is therefore the first step in any React Compiler introduction and gives a clear work plan: fix all reported errors, then enable the compiler.
6. useMemo and useCallback: before and after
What happens to existing useMemo and useCallback code after enabling the React Compiler? The compiler respects and keeps existing manual memoization calls, it does not override them. That means existing code keeps working, and the compiler additionally optimizes the parts that are not yet manually cached. In the long run, manual useMemo and useCallback calls in compiler-optimized codebases can be treated as a hint that this code used to be problematic without the compiler, and can be removed step by step.
The React Compiler also makes React.memo redundant in many places. React.memo prevents a component from re-rendering when its props have not changed, but only if the props objects are referentially stable. The compiler ensures that objects and functions passed as props are referentially stable when their inputs have not changed. That makes React.memo redundant in many places, since the compiler optimizes at a more granular level.
7. Limits of the React Compiler: what it cannot do
The React Compiler has clear limits that need to be understood in order to have realistic expectations. It only optimizes rule-compliant code: any component that violates the Rules of React is skipped by the compiler, or a build warning is generated. That means a legacy codebase with many violations barely benefits from enabling the compiler until the violations are fixed. The compiler is not a magic tool that gives performance to unclean code, it is a tool for clean code that adds performance automatically.
Another limit: the React Compiler analyzes code boundaries at build time. It cannot optimize dynamic runtime decisions. If, for example, a component renders depending on an external mutable data structure (an array that is mutated directly), the compiler cannot see that the value has changed, and could incorrectly return cached values. That is why immutability of state and props is not just a style principle but a functional requirement for correct compiler optimizations.
// eslint.config.js, configuring eslint-plugin-react-compiler
import reactCompiler from 'eslint-plugin-react-compiler';
export default [
{
plugins: {
'react-compiler': reactCompiler,
},
rules: {
// Report all Rules of React violations as errors
'react-compiler/react-compiler': 'error',
},
},
];
// Examples of Rules of React violations, the compiler will skip these components
// VIOLATION 1: Direct mutation of state or props
function MutatingComponent({ items }: { items: string[] }) {
// Direct mutation: items.push modifies the prop directly
// React Compiler cannot safely memoize this component
items.push('new item'); // Rules of React violation
return <ul>{items.map(i => <li>{i}</li>)}</ul>;
}
// CORRECT: return a new array instead of mutating
function NonMutatingComponent({ items }: { items: string[] }) {
const displayItems = [...items, 'new item']; // new reference, safe to cache
return <ul>{displayItems.map(i => <li>{i}</li>)}</ul>;
}
// VIOLATION 2: Conditional hook call
function ConditionalHookComponent({ isAdmin }: { isAdmin: boolean }) {
if (isAdmin) {
const [count, setCount] = useState(0); // Hook inside condition
return <div>{count}</div>;
}
return <div>No access</div>;
}
// CORRECT: always call hooks unconditionally, use them conditionally
function CorrectHookComponent({ isAdmin }: { isAdmin: boolean }) {
const [count, setCount] = useState(0); // always called
if (!isAdmin) return <div>No access</div>;
return <div>{count}</div>;
}
8. Introducing React Compiler into existing codebases
Introducing the React Compiler into an existing codebase is a structured process that should be carried out in phases. Phase 1: install eslint-plugin-react-compiler in warn mode and document all reported violations. That gives a complete overview of the effort involved. Phase 2: prioritize violations by frequency and criticality. Direct state mutations are typically the most common and at the same time the most important to fix. Phase 3: introduce fixes systematically, starting with the simplest components.
Phase 4: enable the React Compiler in "annotation only" mode, in which it only compiles files explicitly marked with the 'use memo' pragma. That enables a step-by-step rollout: compile individual, already-cleaned files first, test, then add more. Phase 5: once all files are clean and tests pass, enable the compiler in full mode. Performance profiling before and after the compiler gives quantitative data on the actual gain. Typical improvements: 10 to 30 percent fewer re-renders in complex component trees.
9. Manual memoization vs. React Compiler compared
The direct comparison shows what actually changes with the React Compiler, not just technically, but also in day-to-day development.
| Aspect | Manual memoization | React Compiler | Benefit |
|---|---|---|---|
| Dependencies | Maintained manually, stale bugs possible | Compiler analyzes automatically | No stale closure bugs from deps |
| Granularity | Per hook call, often too coarse | Sub-expressions, very fine grained | More precise optimizations |
| Overuse | Frequently used for simple computations | Only where it makes sense | No useless overhead |
| Code length | Lots of boilerplate (useMemo, useCallback) | No memoization code needed | Shorter, more readable code |
| Consistency | Depends on developer discipline | Always consistent | Uniform performance baseline |
| Requirements | Works with any code | Rules of React must be followed | Migration path needed for legacy code |
| React.memo | Often necessary for stable props | Often redundant | Less wrapper boilerplate |
| Debugging | Stale deps hard to find | ESLint reports violations early | Earlier error detection |
The table shows that the React Compiler wins in almost every dimension, with one important exception. It requires rule-compliant code, which means a migration effort for legacy codebases. Whoever invests that effort gets a codebase that is more consistently performant, contains less memoization boilerplate and is structurally cleaner, independent of the compiler benefit.
Mironsoft
React performance, compiler migration and codebase modernization
Want to introduce React Compiler in your project?
We analyze existing React codebases for Rules of React violations, fix them systematically and support the step-by-step activation of the React Compiler, from ESLint audit to full compiler integration.
Compiler audit
Set up eslint-plugin-react-compiler and document and prioritize all violations
Fixing violations
Systematically eliminate direct mutations, conditional hooks and side effects in render functions
Activation
Step-by-step compiler activation with performance measurement and regression testing
10. Summary
The React Compiler is not a magic performance solution but a precise tool for clean React code. It analyzes component code at build time and automatically inserts memoization, more precise and more consistent than manual useMemo and useCallback. It makes dependency arrays unnecessary, eliminates stale closure bugs caused by incorrect deps and significantly reduces memoization boilerplate. React.memo becomes redundant in many places. The requirement is rule-compliant code: no direct mutations, no conditional hook calls, no side effects in render functions.
For new projects, introducing the React Compiler from the start is simple: install the ESLint plugin, enable the compiler, write clean code. For existing codebases, a step-by-step approach is the right path: first find and fix the violations with the ESLint plugin, then test the compiler in annotation-only mode, then enable it fully. The performance gains are real, typically 10 to 30 percent fewer re-renders in complex component trees, but they are a byproduct of the actual goal: a codebase that consistently follows the Rules of React.
React Compiler, the essentials at a glance
What the compiler does
Build-time analysis and automatic memoization, more precise than manual useMemo/useCallback. No new runtime, no semantic change, only optimization.
Requirements
Follow the Rules of React: no state mutations, no conditional hooks, no side effects in render. eslint-plugin-react-compiler checks for violations.
Activation
babel-plugin-react-compiler for Babel/Vite. Native in Next.js 15 experimental.reactCompiler. Step-by-step rollout possible with annotation-only mode.
Effect on code
useMemo, useCallback and React.memo become largely unnecessary. Dependency arrays disappear. Codebase shorter, more readable and consistently performant.