2026

April 22, 2026

The React Compiler at Eighteen Months: The Arc, the Debates, and What's Next

Since React 19 shipped with the compiler stable, the ecosystem has run through the predictable phases: framework integration, tooling maturity, community debate. A retrospective on the last eighteen months and what the React team has signaled comes next.

S
Sascha Becker
Author

11 min read

The React Compiler at Eighteen Months: The Arc, the Debates, and What's Next

React 19 shipped with the React Compiler stable at the end of 2024. Eighteen months later, the ecosystem has run through the phases you would expect from any large platform-level change: announcement, framework integration, tooling maturity, and then the slower, messier work of debating what the thing actually means.

This post is a retrospective on that arc and a reading of what the React team has signaled for what comes next. It is not an explainer, though the explainer bits are there as reference. I have not done a large production migration myself. What follows draws on the public documentation, RFCs, React team talks, and the early-adopter write-ups linked at the end.

The Eighteen-Month Arc

Since the stable announcement at React Conf 2024, the compiler's story has followed a predictable shape: release, integration, tooling, debate.

Release and early integration. In the months after React 19 shipped, the compiler's practical surface was mostly configuration. Next.js, Expo, TanStack Start, and the Vite-based frameworks each wired it into their build pipeline. For a new app, the story became "it's on by default, and you can turn it off if you need to." For existing apps, a migration path emerged around the ESLint plugin as a leading indicator of readiness.

The quiet middle. The middle of the arc was quieter than the initial hype suggested. Teams that adopted early did so with more skepticism than fanfare, and the wins they reported were the boring kind: fewer re-render bugs surfacing in code review, a drop in the "why is this slow" bug category, a codebase that stopped accumulating new memoization sprawl. The dramatic benchmark posts the community braced for never really arrived, because the compiler's biggest impact is avoiding bugs rather than posting headline numbers.

The ecosystem reckoning. By late 2025 the conversation shifted from "should we adopt" to "what do we do about the libraries that break it." That is where most teams are still stuck. The Rules of React, which were always true, became enforceable at build time, and a surprising amount of the ecosystem had been quietly bending them. Older state libraries, legacy forms, some drag-and-drop implementations, a handful of well-loved utility hooks. The compiler did not break them; it made it visible that they had been broken all along.

The current state, in a sentence: greenfield is solved, brownfield is a project.

What the Compiler Does

In brief, for reference. The compiler is a build-time transform. It reads your components, assumes they follow the Rules of React, and inserts memoization where it helps. There is no runtime cost from the compiler itself; the output is plain React. The granularity is better than what a human writes: where a hand-written useMemo memoizes a whole derived object, the compiler can memoize individual sub-expressions inside it, so changing one field does not invalidate the others.

Practically, code like this:

tsx
const DashboardRow = memo(({ entity, onSelect }: Props) => {
const formatted = useMemo(
() => ({
label: formatLabel(entity),
total: entity.items.reduce((sum, i) => sum + i.value, 0),
status: entity.state === "active" ? "green" : "red",
}),
[entity],
);
const handleClick = useCallback(() => {
onSelect(entity.id);
}, [entity.id, onSelect]);
return (
<Row onClick={handleClick}>
<Label color={formatted.status}>{formatted.label}</Label>
<Total>{formatted.total}</Total>
</Row>
);
});

Becomes code like this in a compiler-enabled codebase:

tsx
function DashboardRow({ entity, onSelect }: Props) {
const label = formatLabel(entity);
const total = entity.items.reduce((sum, i) => sum + i.value, 0);
const status = entity.state === "active" ? "green" : "red";
return (
<Row onClick={() => onSelect(entity.id)}>
<Label color={status}>{label}</Label>
<Total>{total}</Total>
</Row>
);
}

The memo wrapper, the useMemo, the useCallback: gone. The most visible payoff during interaction is a change in the scaling shape of re-renders. The classic failure case is a list where one bad dependency in a parent's useCallback forces every row to re-render on every keystroke; cost scales with list size until input lag shows up. The compiler removes that scaling. Rows whose props did not change stay untouched regardless of list size.

The costs, for completeness: build time goes up in the tens-of-percent range on public benchmarks (incremental builds are mostly unaffected), and bundle size ticks up a low single-digit percent from inlined memoization helpers. Both are project-specific; do not trust any exact number you read without measuring your own.

Where the Compiler Fails

Eighteen months of production use has sharpened the list of things the compiler will not touch. The four patterns below come up in every migration write-up I have read.

1. Mutating props or closures during render

tsx
function Row({ entity }: Props) {
entity.lastSeen = Date.now(); // mutation during render
return <span>{entity.name}</span>;
}

The compiler refuses to transform this. Fix the code.

2. Reading a ref during render

tsx
function Tooltip() {
const ref = useRef<HTMLDivElement>(null);
const width = ref.current?.offsetWidth; // reads ref in render body
return <div ref={ref}>{width}px</div>;
}

Move the read into an effect or useLayoutEffect. The compiler flags this but does not rewrite it.

3. Legacy class components

Class components are not compiled at all. If you still have React.Component subclasses, they run as they always did. Rarely a problem in new projects; worth knowing in older codebases.

4. The "use no memo" escape hatch

Sometimes the compiler is wrong, or the cost of making a component compiler-safe is not worth paying yet. You can opt out one function at a time:

tsx
function ComplicatedLegacyThing() {
"use no memo";
// compiler skips this function, treats it as plain React
...
}

The Migration Path Most Teams Follow

Based on the React docs and the integration guides for Next.js, Expo, and TanStack Start, the commonly recommended sequence is short:

  1. Update React to a version that supports the compiler (React 19 or later).
  2. Install the ESLint plugin first. eslint-plugin-react-compiler flags Rules-of-React violations without any runtime change. Fix what it finds before touching compiler config.
  3. Enable the compiler in annotation-driven mode. Compile one or two leaf components first and observe.
  4. Switch to inference mode. Let the compiler decide which files to handle. Run tests, profile a real interaction.
  5. Remove the manual memoization. useMemo, useCallback, React.memo calls the compiler now handles can be deleted in bulk.
  6. Consider strict mode, once the codebase is clean, so violations throw instead of silently skip.

Teams that skip directly to step 5 tend to produce one enormous PR that sits open for weeks. Steps 1 and 2 land independently and improve code quality on their own.

What's Still Contested

Three things the community has not settled eighteen months in.

Rules of React as an enforceable contract

The Rules of React existed before the compiler, but they were aspirational. The compiler turns them into a build-time boundary: violate them and your component silently opts out of memoization. A vocal minority argues this quietly imposes a stricter programming model than React itself ever promised. The counter-argument is that the Rules were never really optional; the compiler just made the consequences visible. Both camps are partially right, and the tension shows up most sharply in older libraries that predate the Rules being written down.

"use no memo" as permanent technical debt

The escape hatch is easy to reach for, and that worries the people who have watched codebases accumulate similar markers over years. The usual comparisons are @ts-ignore and // eslint-disable: useful release valves that age poorly when they stop being revisited. Others argue the directive is load-bearing precisely because it lets you ship without a full refactor, and that treating it as a permanent marker is misuse, not a property of the feature. What nobody disputes anymore is that "use no memo" count is a legitimate codebase-health metric.

Compiler vs runtime optimizers

For most apps the compiler is enough. For list-heavy specialist UIs (trading dashboards, log viewers, spreadsheets) a block-based reconciler like Million.js still measurably wins. The two layers solve different problems: the compiler reduces how often components re-render, runtime optimizers change how fast each re-render is. Some teams run both. The interaction between them is fine in practice but not well-documented anywhere in a way that feels settled. I suspect this is where someone will eventually write the definitive post.

What Comes Next

The public signals from the React team, Meta engineering posts, and active RFCs point in five directions. None of these are promises; treat them as directional.

Finer-grained compilation control

The current modes are coarse: on, off, annotation-driven. What teams seem to want is per-component hints, compile aggressively for this one, conservatively for that one, never for this legacy island, plus better tooling to see what the compiler actually did. Expect directives that narrow the current all-or-nothing tradeoff.

Compiler-aware Server Components

RSC already ships memoization-friendly output for client islands. The next step is tightening the serialization boundary, so the payload the browser has to hydrate gets smaller when the compiler can prove that a value does not need to cross. This is where the biggest performance wins for real apps live: reducing hydration cost matters more than reducing re-render counts, because hydration is what users feel on cold loads.

useEvent converging

The long-discussed primitive for stable event callbacks that read the latest state has been in RFC for years. The compiler's purity analysis is the piece that makes its semantics tractable to verify, which is why the proposal stalled before the compiler existed. Expect it to ship in some form.

React Native

Native view diffing is more expensive than web reconciliation, so auto-memoization matters disproportionately there. Expo has shipped compiler support; React Native itself has been slower. The pressure for parity is growing because the benefit per component is larger.

Developer tooling

The missing piece is a DevTools panel that shows, per component, what the compiler did and why. Right now the ESLint plugin tells you what was skipped; a visualizer for what was optimized would make the compiler's work legible in a way that feels like the RSC tree visualizer. This is not on any public roadmap I know of, but it is the obvious next tool, and the kind of thing a community plugin can ship before the React team does.

My Take on the Road Ahead

Eighteen months in, the compiler's legacy is not going to be its benchmark numbers. It will be the category of bug it retired. "You forgot a useCallback dependency" is not a conversation anymore. Nor is "should this component be memoized?" The answer is yes, always, and the compiler handles it.

The more interesting question is what the Rules of React become now that a compiler enforces them. For years they were a list of things good React code happened to do; now they are a build-time contract that rejects code that does not comply. Some of the ecosystem's oldest libraries are still adjusting. The next year of the compiler's story is probably less about the compiler itself and more about what the library ecosystem looks like after everything has been made compiler-safe.

For new projects, the decision is easy: turn it on. For existing projects, the decision is whether to fix your oldest code or ship "use no memo" as a commitment device. Both are legitimate tradeoffs; both are worth being deliberate about.

When you see a useMemo or useCallback in 2026, treat it the way you would treat a manual for loop in modern JavaScript. Usually fine. Occasionally necessary. Mostly a sign that the author wrote this before better tools existed.


S
Written by
Sascha Becker
More articles