Posted on: 20/09/2026(updated)
The React Compiler is a build-time tool that automatically memoises your components. It runs during your build and rewrites your components with optimal memoisation.
Right now, somewhere in your codebase, a component wrapped in React.memo still re-renders on every keystroke. The wrapper seems to work. The component is memoised, and callbacks are wrapped in useCallback.
But if somewhere up the tree, a parent component passes an inline arrow function as a prop, or a child component spreads props from a parent component, memo will see it and re-render the component every time as if the wrapper wasn't there.
React doesn't warn you if you happen to have a semi-broken optimisation, as long as the app works.
Meta argues manual memoisation doesn't scale, and that the React Compiler does the job better than a developer could by hand. It automatically detects dependencies within your components, and sets optimal memoisation states in the compiled code.
This means you don't have to add useMemo, useCallback, or React.memo everywhere in your components.
Instead of inserting useMemo and useCallback calls into your final build, on every call it allocates a closure, and on every render a new dependency array, in order to decide whether the cached value should be reused, and from there creates its own caching mechanism. So for instance, this function:
function TodoList({ todos, tab }) {
const visibleTodos = filterTodos(todos, tab);
return (
<ul>
{visibleTodos.map(todo =>
<li key={todo.id}>
{todo.text}
</li>)
}
</ul>
);
}
Will compile to something like this:
function TodoList(t0) {
// request 5 cache slots for this component
const $ = _c(5);
const { todos, tab } = t0;
let visibleTodos;
if ($[0] !== todos || $[1] !== tab) {
// an input changed, so recompute and store result
visibleTodos = filterTodos(todos, tab);
$[0] = todos;
$[1] = tab;
$[2] = visibleTodos;
}
else {
// inputs are identical, so reuse cached value
visibleTodos = $[2];
}
// ...the same pattern repeats for the JSX itself
}
t0 parameter is just a props object, renamed and destructured._c(5) call creates an array with five slots attached to the component instance.memo and dependency arrays do.useMemo can't.Caching a filtered array doesn't stop a child component from re-renders. The compiler skips re-renders through a different mechanism: if a component receives the exact same element object it received last time, React skips rendering since it cannot produce a different output.
This is similar to how passing components as children doesn't produce re-renders.
The compiler is a Babel plugin, so setup depends on your build tool, but it must run first in the Babel plugin pipeline because it needs your original, untransformed source code to analyze it.
For Vite, a small Babel bridge along with the plugin is needed:
npm install --save-dev --save-exact babel-plugin-react-compiler@latest
npm install --save-dev @rolldown/plugin-babel
// vite.config.js
import { defineConfig } from 'vite';
import react, { reactCompilerPreset } from '@vitejs/plugin-react';
import babel from '@rolldown/plugin-babel';
export default defineConfig({
plugins: [
react(),
babel({
presets: [
reactCompilerPreset()
],
}),
],
});
Compiler support in Next.js v16 is stable, and enabling it is a single config flag:
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactCompiler: true,
};
export default nextConfig;
Note: Next.js normally doesn't use Babel. It compiles with SWC (a Rust-based tool adopted by Next.js because it's a lot faster than Babel, while the React Compiler only runs through Babel). So enabling the compiler reattaches Babel to a pipeline built to avoid it, and your development and production builds may get a bit slower.
The compiler targets React 19 by default.
The compiler can only memoize code it knows is safe to memoize, which is based on the Rules of React:
When a component breaks those rules, the compiler skips the component entirely.
You can see which components made it through in the Components tab of React DevTools, where compiled components get a small ✨ badge, and where no badge indicates a rule violation.
Before enabling anything, run this in your project:
npx react-compiler-healthcheck
It scans the codebase and reports how many components can be compiled successfully. If you enable the compiler globally and one component misbehaves, you can momentarily opt it out while you investigate like this:
function LegacyDataGrid() {
'use no memo';
// skipped by the compiler entirely
}