open atlas
↑ Back to track
React, zero to senior RCT · 08 · 04

React Compiler, honestly: automated memo with silent bailouts

The React Compiler memoizes at build time by proving the Rules of React hold. Where it cannot prove them it bails out silently — code just stays unmemoized. The eslint plugin is the contract surface, manual memo keeps narrow jobs, and wins are measured in the Profiler.

RCT Senior ◷ 17 min
Level
FoundationsJuniorMiddleSenior

A platform team enabled the React Compiler on a 400-component product, announced “manual memoization is legacy now” in the engineering channel, and opened a cleanup PR deleting useMemo and useCallback across the codebase. Two weeks later the support queue lit up: the order-history table — their heaviest screen — was visibly slower. The Profiler showed it re-rendering top to bottom on every parent update, no memoization anywhere. The component had a sort routine that mutated an array it had received via props — rows.sort() in render, a Rules of React violation that had sat harmless for years. The compiler saw it, could not prove the component safe to memoize, and did what it always does with code it cannot verify: skipped the whole component. No error, no build warning in their setup, no runtime flag — the output was simply the original, unmemoized code. The hand-written useMemo that used to paper over the cost had been deleted in the cleanup, so the screen regressed while the dashboard said “compiler: enabled”. They found it only because someone ran the eslint plugin and saw the violation the compiler had silently walked away from. The compiler is real and the wins are real — but it is an optimizer with a veto, and the veto is exercised in silence.

What it automates: memoization as a compiler pass

The React Compiler is a build-time transform (a Babel plugin, slotting into Vite/Next/Metro pipelines) that rewrites components and hooks to be memoized by default. It statically analyzes each function, tracks which values depend on which inputs, and caches intermediate results — values, JSX subtrees, function identities — so that a re-render only recomputes what its changed inputs actually reach. Three honest clarifications about scope:

  • It is finer-grained than your useMemo. Hand-written memoization works at the granularity you bothered to write; the compiler memoizes per expression, including JSX elements themselves, so an unchanged subtree is skipped even when the component re-runs. It routinely outperforms decent hand-memoization on breadth alone.
  • It changes none of your semantics — when it applies. Compiled output must behave identically to source for rule-following code. The compiler’s entire license to rewrite is the assumption that your components are pure, props and state are not mutated, and hooks are called unconditionally — the Rules of React, now load-bearing.
  • It does not make slow code fast. It removes unnecessary re-render cost. An expensive calculation that genuinely must run on changed inputs runs exactly as before; a chart that re-renders because its data really changed re-renders. Teams measuring “compiler made p95 commit time better by 30%” are measuring avoided re-renders, not faster ones.

The bailout: silent by design

When the compiler cannot prove a component follows the rules, it does not emit a half-optimization and does not fail the build — it skips the component entirely, leaving the original code untouched. Triggers it cannot verify past include: mutating props, state, or values derived from them (rows.sort(), obj.x = y on a non-local); reading or writing refs during render; dynamic property access patterns it cannot reason about; values that escape into closures with unclear lifetimes. The output difference is invisible at runtime: the component works, renders correctly, and simply re-renders as often as it did before the compiler existed.

This silence is a deliberate engineering choice — correctness over coverage; a wrong memoization corrupts UI state, a skipped one only costs CPU. But it creates the failure mode from the Hook: “compiler enabled” is not “codebase memoized.” Coverage is a property you must observe, not assume. Two observation points exist: the eslint plugin at write time, and React DevTools at runtime — compiled components are badged with a “Memo ✨” marker in the components panel, so a spot check of your heaviest screens tells you whether the optimizer actually took the deal.

Quiz

After enabling the compiler, a team deletes manual useMemo calls in a heavy component. The screen gets slower, but the build is green and the app works. What most likely happened?

The eslint plugin is the contract surface

eslint-plugin-react-compiler runs the same analysis as the compiler and reports, at write time, every place the compiler will refuse. That changes its role from style checker to coverage contract: a violation is not a nitpick, it is a named component that will silently stay unmemoized. The operational pattern for adoption:

  1. Run the plugin across the codebase before enabling the compiler — the violation list is your bailout map.
  2. Fix violations in the hot paths first (they are where memoization is worth money); accept cold-path violations consciously.
  3. Enforce zero new violations in CI. From then on, the compileable surface only grows, and “the compiler covers this screen” is a checkable claim instead of a vibe.

Together these three steps mean coverage becomes auditable before you delete a single line of manual memo — and that order matters, because the regression in the Hook happened precisely because step three was skipped entirely.

What manual memoization still does

The compiler does not retire the manual toolkit; it narrows it to jobs the compiler structurally cannot do:

  • Cross-module boundaries you do not compile. A component library published to npm ships whatever its build produced; consumers’ compilers do not reach into node_modules. If your library’s components need memoization guarantees, React.memo and stable callbacks remain part of the published contract.
  • Non-component caching. Module-level caches, memoized selectors (reselect and friends), expensive pure functions called outside render — the compiler memoizes within components and hooks; it does not invent caches for your data layer.
  • Bailed-out components, knowingly. Until a violation is fixed, hand memoization is the bridge — which is exactly why deleting it should follow verified coverage, not precede it.
  • Semantics, not speed. A useMemo that guards a referential identity a downstream effect depends on (a deps array, a context value) encodes correctness, and the compiler preserves but does not replace your reasoning about it.

Maturity and adoption, honestly

Where it stands: years of production use at Meta scale preceded public stability, the first stable release shipped in late 2025 after a long RC, and major frameworks expose it as a config flag. What that does not mean: flip-the-switch-and-forget. The honest adoption posture is incremental — the compiler supports gradual rollout (annotation mode compiles only components marked "use memo"; directory-scoped Babel overrides confine it per package or per area), and teams that succeed run weeks of canary comparing field metrics before widening scope. Ecosystem code with rule violations is common in older dependencies and legacy corners; each is a silent bailout, not a crash, so the blast radius of enabling broadly is performance unevenness, not breakage.

And measure like an adult: record the React DevTools Profiler on your three heaviest interactions before enabling; enable; re-record. Look for fewer components rendering per commit and lower commit durations; check the Memo badge on the components you care about; watch INP in field data over a release cycle. Teams report real numbers — Meta cited meaningful wins on Quest Store-class surfaces — but your number depends on how much unnecessary re-rendering you had and how much of your tree the compiler can prove. A codebase already disciplined about memo may see single-digit percent; a codebase that never memoized may see interactions get dramatically cheaper. Faith is not a metric.

Quiz

A team publishes a React component library to npm and asks whether the compiler means they can drop React.memo from their public components. What is the correct call?

Recall before you leave
  1. 01
    Explain what the React Compiler actually does to a component, what makes it refuse, and why the refusal is dangerous operationally despite being safe technically.
  2. 02
    Define the adoption playbook for the compiler at a 400-component scale, including what manual memoization remains for.
Recap

The React Compiler moves memoization from a hand-written discipline to a compiler pass: at build time it proves, per component, that the Rules of React hold — purity, no mutation of props or state, unconditional hooks — and where the proof goes through it rewrites the function to cache values, function identities, and JSX subtrees per expression, finer-grained than the useMemo calls it replaces. Where the proof fails, it walks away silently: the original code ships, correct and unmemoized, indistinguishable at runtime from the compiler never having run. That veto-in-silence is the operational story. The order-history regression happened not because the compiler broke anything but because a rows.sort() in render made it refuse, and the manual memo that had been compensating was deleted on the assumption of coverage. The contract surface is eslint-plugin-react-compiler — same analysis, write-time verdicts, each violation naming a component the compiler will skip — enforced in CI so coverage becomes checkable; the audit trail is the DevTools Memo badge and Profiler commit times, plus field INP. Manual memoization narrows to what the compiler structurally cannot touch: published library components (nobody compiles your node_modules), caches outside components, and conscious bridges over known bailouts. Maturity is real — Meta-scale production years, stable since late 2025, framework flags — and adoption is still incremental by design: annotation or directory scoping, canary with before/after profiles, cleanup only after the wins are measured. The compiler automates the memoization lesson’s contract; it does not repeal it. Now when you see a screen that got slower after enabling the compiler — green build, working app, just slower — your first move is to open the DevTools components panel: if the Memo badge is missing from the component you care about, the eslint plugin will name the violation the compiler silently walked away from.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 6 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.