Introduction
For years, React developers have manually optimized re-renders with useMemo, useCallback, React.memo, and various state-management heuristics. The React Compiler (formerly known as React Forget) changes this paradigm entirely. It is a build-time tool that automatically memoizes React components, hooks, and values by analyzing reactive dependencies at compile time, eliminating the need for manual optimization hooks.
The Problem with Manual Memoization
Consider a typical filtered list component:
function UserList({ users, filter }) {
const filtered = useMemo(
() => users.filter(u => u.name.includes(filter)),
[users, filter]
);
const handleClick = useCallback((id) => {
console.log('Clicked:', id);
}, []);
return (
<ul>
{filtered.map(user => (
<UserRow key={user.id} user={user} onClick={handleClick} />
))}
</ul>
);
}
const UserRow = React.memo(({ user, onClick }) => (
<li onClick={() => onClick(user.id)}>{user.name}</li>
));
This works, but it is verbose, error-prone (missing or incorrect dependency arrays), and easy to forget. A missing useMemo on a large list causes unnecessary re-renders; an incorrect dependency array causes stale closures.
What Is the React Compiler?
The React Compiler is a build-time plugin (Babel / SWC) that automatically understands the reactivity model of React components. It transpiles your code to include memoization logic automatically:
// You write this:
function Welcome({ name }) {
const greeting = `Hello, ${name}!`;
return <p>{greeting}</p>;
}
// Compiler outputs effectively this:
function Welcome({ name }) {
const $ = React.cache(/* ... */);
let greeting = $.refresh('greeting', name => `Hello, ${name}!`, [name]);
return $.memo('element', () => <p>{greeting}</p>, [greeting]);
}
The compiler detects which values depend on props, state, or other reactive values and only recomputes them when those dependencies change.
How It Works
The compiler operates in three phases:
Reactive Value Detection — It walks the Abstract Syntax Tree (AST) to identify which variables derive from props, state, hooks, or other reactive sources.
Dependency Graph Construction — It builds a dependency graph connecting reactive values. Any value derived solely from non-reactive sources is treated as static.
Memoization Code Generation — It emits optimized JavaScript with built-in caching invalidation. The runtime uses a fine-grained dependency tracking mechanism similar to signals.
Key rules the compiler follows:
| Rule | Behavior |
|---|---|
| Props and state are always reactive | Any derived value is memoized automatically |
| Non-reactive constants are hoisted | No memoization overhead added |
| Callbacks are stable by default | No need for useCallback |
| Component bodies are optimized | Only re-execute when reactive deps change |
Integration with Build Tools
Vite
// vite.config.js
import react from '@vitejs/plugin-react';
export default {
plugins: [
react({
babel: {
plugins: [['babel-plugin-react-compiler', { target: '18' }]],
},
}),
],
};
Next.js
// next.config.js
module.exports = {
experimental: {
reactCompiler: true,
},
};
What You Can Remove
With the React Compiler enabled, you no longer need:
useMemo()— automatic value memoizationuseCallback()— automatic function stabilityReact.memo()— automatic component memoization (the compiler wraps render output)- Manual dependency arrays for most cases
The compiler can even optimize patterns that are difficult to handle manually, such as conditional hooks — though you should still follow the rules of hooks.
Caveats and Best Practices
While the compiler handles a vast majority of cases, some scenarios warrant caution:
| Caveat | Recommendation |
|---|---|
Mutable refs — useRef mutations are not tracked | Use useState or trigger explicit re-renders |
| External stores — values not from React | Wrap in useSyncExternalStore |
Complex side effects — useEffect with non-reactive deps | Add explicit deps if needed |
| Legacy context — Class component contexts | Migrate to modern useContext |
The compiler is most effective on codebases that follow standard React patterns. Heavily imperative code may see fewer benefits.
Conclusion
The React Compiler represents a fundamental shift from manual to automatic memoization. By analyzing reactive dependencies at build time, it eliminates an entire category of performance bugs and boilerplate. For existing projects, enabling the compiler requires minimal configuration; for new projects, it changes how you think about React performance — you simply write declarative components and let the compiler handle the rest.
