React Compiler - Automatic memoization that eliminates manual useMemo, useCallback, and React.memo
Recipe
# Install the compiler and its Babel plugin
npm install -D babel-plugin-react-compiler
npm install -D react-compiler-healthcheck # optional: check compatibility// babel.config.js
module.exports = {
plugins: [
["babel-plugin-react-compiler", {
// target defaults to "19" for React 19
}],
],
};// next.config.js (Next.js 15+)
module.exports = {
experimental: {
reactCompiler: true,
},
};When to reach for this: Enable the React Compiler for any React 19 project to automatically optimize re-renders. It replaces the need for useMemo, useCallback, and React.memo in the vast majority of cases.
Working Example
// Before: Manual memoization everywhere
import { useMemo, useCallback, memo } from "react";
type Todo = { id: string; text: string; completed: boolean };
const TodoItem = memo(function TodoItem({
todo,
onToggle,
}: {
todo: Todo;
onToggle: (id: string) => void;
}) {
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
</li>
);
});
function TodoList({ todos }: { todos: Todo[] }) {
const [filter, setFilter] = useState<"all" | "active" | "completed">("all");
const filteredTodos = useMemo(() => {
switch (filter) {
case "active": return todos.filter((t) => !t.completed);
case "completed": return todos.filter((t) => t.completed);
default: return todos;
}
}, [todos, filter]);
const handleToggle = useCallback((id: string) => {
// toggle logic
}, []);
const stats = useMemo(() => ({
total: todos.length,
active: todos.filter((t) => !t.completed).length,
completed: todos.filter((t) => t.completed).length,
}), [todos]);
return (
<div>
<p>{stats.active} items left</p>
<ul>
{filteredTodos.map((todo) => (
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
))}
</ul>
</div>
);
}// After: With React Compiler -- no manual memoization needed
import { useState } from "react";
type Todo = { id: string; text: string; completed: boolean };
function TodoItem({
todo,
onToggle,
}: {
todo: Todo;
onToggle: (id: string) => void;
}) {
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
</li>
);
}
function TodoList({ todos }: { todos: Todo[] }) {
const [filter, setFilter] = useState<"all" | "active" | "completed">("all");
const filteredTodos = (() => {
switch (filter) {
case "active": return todos.filter((t) => !t.completed);
case "completed": return todos.filter((t) => t.completed);
default: return todos;
}
})();
function handleToggle(id: string) {
// toggle logic
}
const stats = {
total: todos.length,
active: todos.filter((t) => !t.completed).length,
completed: todos.filter((t) => t.completed).length,
};
return (
<div>
<p>{stats.active} items left</p>
<ul>
{filteredTodos.map((todo) => (
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
))}
</ul>
</div>
);
}What this demonstrates:
- The "before" version requires
useMemo,useCallback, andReact.memoto avoid unnecessary re-renders - The "after" version is plain, natural React code -- the compiler adds memoization at build time
- The code is simpler, easier to read, and equally performant
Deep Dive
How It Works
- The React Compiler is a build-time tool (Babel plugin) that analyzes your components and hooks, then automatically inserts memoization.
- It understands React's rules of hooks and the rules of React (pure rendering, immutable props/state) to determine what values can be safely cached.
- The compiler transforms your code to memoize: JSX elements, expensive computations, callback functions, and component re-renders -- roughly equivalent to wrapping things in
useMemo,useCallback, andReact.memowhere appropriate. - It performs fine-grained memoization -- it can memoize individual JSX subtrees within a component, not just the entire return value.
- The compiler targets React 19 by default and uses React 19's internal cache API. For React 17 and 18, set
target: "17"ortarget: "18"and install thereact-compiler-runtimepackage. - If the compiler detects code that violates React's rules (e.g., mutating props, side effects during render), it skips that component and leaves it unoptimized. It does not break your code.
- You can use the
"use no memo"directive at the top of a function to opt out a specific component or hook from compiler optimization.
Variations
Gradual rollout with opt-in mode:
// babel.config.js -- only compile files with "use memo" directive
module.exports = {
plugins: [
["babel-plugin-react-compiler", {
compilationMode: "annotation", // only compile "use memo" functions
}],
],
};// Only this component is compiled
function OptimizedList({ items }: { items: string[] }) {
"use memo";
return (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}Opting out a specific component:
function LegacyComponent({ data }: { data: any }) {
"use no memo";
// This component will not be optimized by the compiler
// Useful for code that intentionally breaks React rules
externalMutableStore.value = data;
return <div>{data.label}</div>;
}Health check before adopting:
# Run the health check to see how compatible your codebase is
npx react-compiler-healthcheck --verboseESLint plugin for rule validation:
npm install -D eslint-plugin-react-compiler// eslint.config.js
import reactCompiler from "eslint-plugin-react-compiler";
export default [
{
plugins: { "react-compiler": reactCompiler },
rules: {
"react-compiler/react-compiler": "error",
},
},
];TypeScript Notes
- The compiler works with TypeScript out of the box -- it runs after TypeScript compilation (in the Babel pipeline) or integrates with SWC in Next.js.
- No type changes are needed. The compiler's transformations are transparent to the type system.
"use memo"and"use no memo"are string literal directives (like"use strict") and do not need type declarations.
Gotchas
- Mutating objects or arrays during render -- The compiler assumes pure rendering. Mutating props, state, or variables that are shared across renders causes incorrect caching. Fix: Follow React's rules -- always create new objects/arrays instead of mutating. The ESLint plugin catches most violations.
- Side effects during render -- Code like
console.log(Date.now())or modifying external variables during render may produce stale results when the compiler caches the output. Fix: Move side effects intouseEffector event handlers. - External mutable stores -- Reading from mutable global variables or stores that React does not track (e.g., a plain global object) may break under compiler optimization. Fix: Use
useSyncExternalStorefor external mutable state. - Existing useMemo/useCallback not harmful -- If you already have manual memoization, the compiler works with it. You do not need to remove it before enabling the compiler. Fix: Optionally remove manual memoization for cleaner code, but it is not required.
- Not all code is compiled -- Components that violate React's rules are silently skipped. You may not notice that a component is unoptimized. Fix: Use
eslint-plugin-react-compilerto catch issues and check the compiler's build output for skipped components. - Build time increase -- The compiler adds analysis time to your build. On very large codebases this may be noticeable. Fix: Use the
compilationMode: "annotation"option to compile only opted-in components during migration.
Alternatives
| Approach | When to choose |
|---|---|
| React Compiler | Default for React 19 projects -- automatic, zero-effort optimization |
Manual useMemo / useCallback | React 18 or when you need explicit control over what is memoized |
React.memo | React 18 or for class components the compiler does not handle |
| Million.js | Alternative compiler focused on virtual DOM diffing optimization |
| Manual component splitting | Isolating expensive subtrees when compiler or memoization is insufficient |
"use no memo" directive | Opting out specific components from compiler optimization |
FAQs
What does the React Compiler do at a high level?
- It is a build-time Babel plugin that analyzes components and hooks, then automatically inserts memoization
- It replaces the need for manual
useMemo,useCallback, andReact.memoin most cases - It performs fine-grained memoization -- it can memoize individual JSX subtrees, not just the entire return value
How do I enable the React Compiler in a Next.js project?
// next.config.js
module.exports = {
experimental: {
reactCompiler: true,
},
};For non-Next.js projects, install babel-plugin-react-compiler and add it to your Babel config.
Do I need to remove existing useMemo, useCallback, and React.memo calls?
- No. The compiler works alongside existing manual memoization
- You can optionally remove them for cleaner code, but it is not required
- The compiler will not conflict with or double-memoize these calls
What happens if a component violates React's rules (e.g., mutates props)?
- The compiler silently skips that component and leaves it unoptimized
- It does not break your code -- the component renders as if the compiler was not enabled
- Use
eslint-plugin-react-compilerto detect which components are being skipped and why
How do you opt out a specific component from compiler optimization?
function LegacyComponent({ data }) {
"use no memo";
// This component will not be optimized
externalStore.value = data;
return <div>{data.label}</div>;
}The "use no memo" directive is placed at the top of the function body.
How do you gradually roll out the compiler with opt-in mode?
// babel.config.js
module.exports = {
plugins: [
["babel-plugin-react-compiler", {
compilationMode: "annotation",
}],
],
};Then add "use memo" at the top of functions you want to compile. Only annotated functions are optimized.
How do you check if your codebase is compatible with the compiler?
npx react-compiler-healthcheck --verboseThis scans your codebase and reports which components follow React's rules and which may cause issues.
Can the compiler be used with React 17 or 18?
- Yes. Set
target: "17"ortarget: "18"in the plugin config - You must also install the
react-compiler-runtimepackage - The compiler targets React 19 by default
Gotcha: Why does a console.log inside my component show stale values after enabling the compiler?
- The compiler caches render output when inputs have not changed
- Side effects like
console.log(Date.now())during render may produce stale results - Move side effects into
useEffector event handlers where they belong
Gotcha: My component reads from a global mutable variable and it returns stale data with the compiler. Why?
- The compiler assumes pure rendering and does not track external mutable stores
- Reading from plain global objects that React does not manage breaks under compiler optimization
- Use
useSyncExternalStorefor external mutable state so React can track changes
Does the compiler affect TypeScript types or require type changes?
- No. The compiler's transformations are transparent to the type system
- It runs after TypeScript compilation (in the Babel pipeline) or integrates with SWC in Next.js
"use memo"and"use no memo"are string literal directives and do not need type declarations
Does the React Compiler increase build times?
- Yes, it adds analysis time to the build
- On very large codebases, this may be noticeable
- Use
compilationMode: "annotation"to compile only opted-in components during an incremental migration
Related
- Overview -- Full list of React 19 features
- Server Components -- Server Components do not ship JS so memoization is less relevant there
- Ref as Prop -- Another API simplification in React 19