+1 (781) 556-6029

Adopting the React Compiler on an existing codebase

The React Compiler reached a stable 1.0 release, and the question we now get in nearly every architecture review is some version of "do we turn it on, and do we delete our useMemo calls?" The answer is usually yes to the first and not yet to the second. This is the sequence we use to adopt it on an existing codebase without spending a quarter on it.

What the compiler does is narrow and worth stating precisely: it is a build-time pass that analyzes component and hook bodies and inserts memoization for values and JSX elements that would otherwise be recreated on every render. It is doing by analysis what useMemo, useCallback, and React.memo do by hand. It does not move state, it does not split your contexts, and it does not make a slow render fast — it reduces how often renders happen.

What it will not fix

Worth getting this out of the way, because the disappointment is predictable.

It does not help with a component that is slow when it renders. A 900-row table that takes 40ms to reconcile takes 40ms with the compiler on. Virtualization, narrower subtrees, and less work per row are still the fix.

It does not fix state placement. A useState in the page component that changes on every keystroke still re-renders the page component. The compiler can stop that render from propagating into children whose inputs did not change, which is real, but the wide-wave problem described in Find the re-render before you reach for memo is architectural and stays architectural.

It does not fix network waterfalls, oversized bundles, or an unoptimized LCP image. Those dominate field Core Web Vitals far more often than render cost does, and no compiler pass touches them.

And it does not rescue components that break the rules of React. That is the actual precondition, and it is where the work is.

Step 1: Run the lint rule first, on its own branch

Before you enable any compilation, install the ESLint rule and let it report. In recent releases the compiler rule ships as part of eslint-plugin-react-hooks under the recommended-latest config, so for most teams this is a plugin upgrade rather than a new dependency.

The rule reports the places where the compiler's analysis will bail out, or where your code violates the assumptions it relies on: mutating props or state directly, reading a ref during render, calling hooks conditionally, mutating a value after it has been passed to JSX. Every one of those is already a bug under React's rules — the rule is not imposing new constraints, it is surfacing existing violations that concurrent rendering also punishes, usually less visibly.

Count the reports and read a sample. If you have fewer than a few dozen and they cluster in two or three legacy directories, adoption is a short project. If they are spread evenly across hundreds of components, you have a codebase-hygiene problem that the compiler has usefully measured for you, and fixing it is worth doing whether or not you ever ship compilation.

Do not suppress these with disable comments to get a clean run. A suppressed violation does not become safe; the compiler simply skips that component, and you have paid the review cost for nothing.

Step 2: Compile a subdirectory, not the app

The Babel plugin accepts a filter, so you can scope compilation to a directory:

// babel.config.js
const ReactCompilerConfig = {
  sources: (filename) => filename.includes('src/features/reports'),
};

Pick a directory that is well covered by tests, actively developed, and not the checkout flow. Ship it. Let it sit in production for a sprint.

Two things to watch in that window. First, your existing test suite — compiler bugs are rare, but rule violations the lint rule could not see statically show up as stale values or missing updates, and those look like ordinary regressions in tests. Second, bundle size for the compiled routes. The inserted memoization is code; on component-dense bundles the increase is typically low single-digit percent, and you should know your number rather than assume it is zero.

Step 3: Measure before you claim anything

The compiler's own documentation is careful about expected gains, and so should you be. Take React DevTools Profiler recordings of two or three real interactions before enabling compilation for a directory, then the same recordings after. Compare commit counts and commit durations for the same interaction, and keep both recordings in the pull request.

The realistic outcome on a codebase that was already memoized by hand is a small improvement, sometimes none — the hand-written useMemo calls were already doing that job. The larger gains show up on code that was never memoized at all, which is usually the older half of the app. Say which one you got. "We enabled the React Compiler and interaction commits on the reports route dropped from 11 to 4" is a sentence with evidence behind it; "we adopted the React Compiler for performance" is not.

Field data matters more than lab data here, as always. If the compiler is doing something useful for users, it shows up in INP in your RUM data over the following weeks. If INP does not move, the win was in a lab trace nobody experiences.

Step 4: Widen, then leave the old memoization alone

Expand the sources filter directory by directory as each one stays clean, until it covers the app and you can drop the filter.

At that point the temptation is a large deletion pull request removing useMemo, useCallback, and React.memo. Resist it for now, for three reasons. Hand-written memoization is not harmful when the compiler is on; it is redundant. Removing it is a large, hard-to-review diff with a real chance of deleting a useMemo that was stabilizing an effect dependency rather than a render — a semantic change, not a cleanup. And if you need to disable compilation for a route to debug something, the hand-written memoization is what keeps that route's behavior familiar.

What we do instead: stop writing new memoization once compilation covers a directory, and delete the old calls opportunistically when you are already editing the file for another reason. The codebase converges over a couple of quarters without a single risky pull request.

Versions and prerequisites

The compiler targets React 19. There is a runtime package that allows React 17 and 18 codebases to compile, which is genuinely useful if your upgrade is queued behind something else. But if you are on 17 or 18 and planning an upgrade anyway, do the upgrade first and adopt the compiler on the other side. Two moving parts at once makes a bisect harder, and the React 18 and 19 upgrade sequence is already its own project.

Next.js, Vite, and the other common setups each have a supported way to enable the plugin; for Next.js it is a config flag rather than a hand-written Babel setup. Check build time after enabling — the compiler adds a pass, and on large apps a noticeably slower CI build is a cost worth knowing about before someone else discovers it.

The honest summary

Turn the lint rule on this week; the reports are useful whatever you decide next. Enable compilation on one directory, measure it, and widen if the numbers hold. Expect a modest improvement on hand-tuned code and a larger one on the parts nobody ever profiled. Do not expect it to fix a slow render, a misplaced piece of state, or a 2MB bundle — those are still yours, and they are still where the larger numbers are.