frontend / react / react_compiler.md

React Compiler

6 interview angles 4 min read source

React Compiler

Stable since 1.0 (October 2025), and the default assumption for new React projects in 2026. It changes the advice around memoization, not just a version number — most useMemo/useCallback guidance written before it is now wrong for new code.

What it does

A build-time compiler that analyses your components and inserts memoization automatically. It understands React’s rules well enough to know what can be cached and when a cache must be invalidated.

// You write this
function ProductList({ products, filter }) {
  const visible = products.filter(p => p.category === filter);
  const handleClick = (id) => selectProduct(id);
  return visible.map(p => <Product key={p.id} product={p} onClick={handleClick} />);
}

// The compiler emits the equivalent of useMemo/useCallback around
// `visible` and `handleClick`, with correct dependency tracking.

Previously you’d have wrapped visible in useMemo, handleClick in useCallback, and Product in memo — and probably got a dependency array subtly wrong.

Why manual memoization was bad

Worth being able to articulate, because it’s the “why does this exist” question:

  • Dependency arrays are hand-maintained and drift. Add a variable to a callback, forget the array, get a stale closure. The lint rule catches some cases, not all.
  • It’s easy to memoize the wrong things. useMemo on a cheap computation costs more than it saves — you pay comparison and allocation to avoid a multiplication.
  • It’s viral. Memoizing a child forces you to memoize every prop and callback passed to it, so one memo spreads through a subtree.
  • It’s noise. Business logic buried under caching machinery.

The compiler does it exhaustively and correctly, which humans do not.

What this changes in practice

Before With the compiler
useMemo for derived values not needed — remove it
useCallback for stable handlers not needed
React.memo on components usually not needed
Splitting context to limit re-renders still useful, less critical
Correct dependency arrays in useEffect still required

useEffect dependencies are unchanged. The compiler handles memoization, not effects. Effects still need correct dependencies, still need cleanup, and are still where most React bugs live. See use_effect_deep.md.

The prerequisite: Rules of React

The compiler can only optimise code that follows React’s rules. Where it can’t prove safety, it skips that component rather than risking incorrect behaviour.

The rules that matter:

  • Components and hooks must be pure during render — same inputs, same output, no side effects.
  • Don’t mutate props, state, or values returned by hooks.
  • Hooks called unconditionally, at the top level.
// The compiler will bail out - this mutates a prop during render
function Bad({ items }) {
  items.sort((a, b) => a.n - b.n);      // mutation!
  return items.map(...);
}

// Optimisable - no mutation
function Good({ items }) {
  const sorted = [...items].sort((a, b) => a.n - b.n);
  return sorted.map(...);
}

The practical consequence: code quality now has a performance consequence. Impure components silently don’t get optimised. The ESLint plugin flags what would prevent compilation, which makes it worth running.

Skipped components still work correctly — they’re just not optimised. It fails safe.

Adopting it

npm install -D babel-plugin-react-compiler
// vite.config.js
plugins: [react({ babel: { plugins: ["babel-plugin-react-compiler"] } })]

For an existing codebase:

  1. Run the ESLint plugin first and fix rule violations — that’s the real work.
  2. Enable it on a subset via the sources option, verify, then widen.
  3. Leave existing useMemo/useCallback alone initially. They’re redundant but harmless; the compiler works around them. Remove them opportunistically rather than in one enormous diff.
  4. Measure. Profile before and after rather than assuming.

Next.js 15+ supports it via a config flag.

What it doesn’t fix

Naming what a tool doesn’t do is the senior half of the answer:

  • Slow renders. It avoids unnecessary re-renders; it can’t make an expensive render cheap. That’s virtualisation, pagination, or moving work off the main thread.
  • Large bundles. Different problem — code splitting.
  • Network waterfalls. Suspense and data-fetching design.
  • Bad state architecture. Global state causing wide re-render trees is still bad; the compiler reduces the cost, not the design smell.

Interview angle

  • “What is the React Compiler?” — a build-time compiler, stable since 1.0 in October 2025, that inserts memoization automatically with correct dependency tracking. It’s the default assumption for new React projects in 2026.
  • “Do you still use useMemo and useCallback?” — not in new code with the compiler enabled. It does the same job exhaustively and without hand-maintained dependency arrays. useEffect dependencies are unaffected and still your responsibility.
  • “What stops the compiler optimising a component?” — rule violations, principally mutation during render or impure components. It bails out on anything it can’t prove safe, so the component still works but isn’t optimised. Run the ESLint plugin to find those cases.
  • “How would you adopt it in an existing codebase?” — ESLint plugin first to surface rule violations, enable on a subset, verify with profiling, then widen. Leave existing manual memoization in place initially; it’s redundant but harmless.
  • “Does it make React apps fast?” — it removes unnecessary re-renders. It doesn’t make a slow render fast, shrink a bundle, or fix a network waterfall. Those need virtualisation, code splitting and data-fetching design respectively.
  • “Why was manual memoization a problem worth solving?” — dependency arrays drift and cause stale closures, people memoize cheap computations at a net loss, and memoizing one component forces it through everything passed to it. The compiler applies it exhaustively and correctly.