What it does
The React Compiler is a build-time transform that memoises components and hooks automatically. Where you would have written useMemo, useCallback and React.memo by hand to stop children re-rendering when a parent's unrelated state changes, the compiler inserts equivalent caching based on its analysis of what each value depends on. You write plain React; it emits React that skips work.
In Next.js it is enabled with reactCompiler: true in next.config, which adds the Babel plugin to the build (with Turbopack handling the rest). It is stable as of React 19.1 and Next.js 16 supports it directly.
What we measured
On our own site and two client sites, before and after enabling it, on a throttled mobile profile:
- Interaction to Next Paint on pages with interactive client components (a filterable blog index, a mega-menu, a booking form) improved modestly, in the range of 10 to 40 milliseconds at p75, because keystrokes and menu toggles no longer re-rendered sibling subtrees.
- Bundle size grew by roughly 1 to 3 percent, from the inserted cache slots. Negligible.
- Server rendering and LCP were unchanged, as expected; the compiler's effect is on client re-renders.
- Hand-written
useMemo/useCallbackcould be removed from most components with no measured regression, which simplified the code more than it changed the numbers.
On a marketing site with mostly server components and a handful of islands, the effect is real but small. On an application-heavy page with deep client trees and frequent state changes, it is larger. Set expectations accordingly: this is not a page-speed feature; it is an interaction-smoothness and code-simplicity feature.
What it refused to compile
The compiler is conservative. When it detects a component that breaks the Rules of React, it skips that component (leaving it unoptimised) rather than risk a wrong result, and reports why in the build output or via the ESLint plugin (eslint-plugin-react-compiler, now folded into eslint-plugin-react-hooks). Run the lint rule before enabling the compiler; it lists every component that will be skipped and the reason.
The patterns we hit:
Mutating a value after it was created and used. Building an array with push inside render after passing it to something. The compiler wants values immutable once created. Fix: build the array fully, then use it.
Reading a ref during render. ref.current in the render body, rather than in an effect or handler. Fix: move the read into the handler or effect where it belongs; the compiler is right that the render read was a bug waiting to happen.
Conditional hooks. A hook inside an if. Always a rule violation; the compiler just made it visible.
Mutating props or context values. Assigning to a property of an object received as a prop. Fix: copy, then change.
Hooks called through a non-hook-named function. A helper called getData that internally called useContext. Rename to useData so the compiler (and linter) treat it as a hook.
Every case was a latent bug or a rules violation that happened to work. Fixing them took an afternoon across three sites and the code was better after.
The one runtime bug it exposed
A component relied on a useEffect re-running because a parent re-rendered and passed a new object literal as a prop each time. The effect's dependency was that object; with the parent re-rendering on every state change, the effect ran constantly, and the component's behaviour (refetching on every parent change) was accidentally what the product wanted. The compiler memoised the object at the parent, the prop became referentially stable, the effect stopped re-running, and a "live" panel went stale.
The compiler was correct. The code depended on an accident. The fix was to make the refetch trigger explicit (a key or an interval) rather than a side effect of unstable props. This is the class of bug to expect: behaviour that depended on unnecessary re-renders stops happening. It is rare, and it is worth a full manual pass through interactive features after enabling.
Adopting safely
- Upgrade to React 19.1+ and Next.js 16.
- Lint first. Add the compiler rule to ESLint and fix every reported violation. Each is a component that would otherwise be skipped, and most are real bugs.
- Enable on a branch with
reactCompiler: true. Build. Check the build output for skipped components. - Test interactions by hand on staging: every form, menu, filter, toggle and live panel. You are looking for things that stopped updating.
- Measure INP before and after in field data over a couple of weeks, on the pages with client interaction.
- Remove hand-written memoisation gradually, component by component, where the compiler now covers it. Keep
useMemofor genuinely expensive computations where the intent is documentation as much as optimisation. - Keep the lint rule on. New code that breaks the rules gets flagged at review rather than silently skipped.
When not to bother yet
- A site that is almost entirely server components with no meaningful client interaction: the compiler has little to optimise.
- A codebase with many lint violations and no time to fix them: the compiler will skip most of it and deliver little.
- A dependency on a library that patches React internals in a way the compiler's output confuses (rare now, but check the library's compatibility note).
Where this sits
The compiler is on for every Next.js site we build now, and the lint rule is part of the project template, because the rule catches real bugs regardless of whether the compiler is enabled. The performance gain is welcome; the code simplification is the part we would keep even if the gain were zero.