skip to content

What does the React Compiler (babel-plugin-react-compiler) do to your components at build time, and which hand-written memoization does it make redundant?

level: middleimportance: must knowfreq 60%

answer

  1. build step, not a runtime watcher
  2. caches values and the JSX itself
  3. element identity buys the reconciler bailout
  4. dependencies derived, never hand-written
  5. subsumes useMemo, useCallback, React.memo

basics

~20 s

The React Compiler is a build-time Babel plugin that rewrites components to cache their derived values and their JSX in per-instance slots, reusing them when inputs are unchanged. It replaces most hand-written useMemo, useCallback and React.memo.

solid answer

~50 s

`babel-plugin-react-compiler` runs during the build, not in the browser. For every component and hook it can compile, it analyses the data flow and emits code that keeps a fixed set of cache slots alongside the component instance: each derived value, each inline function, and each piece of JSX is recomputed only when the specific values it reads have changed, compared with `Object.is`. Because the returned element objects keep their identity when nothing relevant changed, React's reconciler bails out of those subtrees — the same win `React.memo` buys, without a wrapper. In practice that makes most `useMemo`, `useCallback` and `React.memo` redundant, and at a finer granularity than a human would write, since it can memoize parts of one component's JSX independently. It is an optimisation only: it never changes what your component renders, provided the code follows the Rules of React.

code

javascript · 19 lines
javascript
import { useMemo, useCallback, memo } from 'react';

// Before the compiler: identity kept stable by hand.
function ProfileManual({ user, onSelect }) {
  const label = useMemo(() => `${user.first} ${user.last}`, [user.first, user.last]);
  const handle = useCallback(() => onSelect(user.id), [onSelect, user.id]);
  return <Row label={label} onSelect={handle} />;
}

// With the compiler enabled: same runtime behaviour, no hooks needed.
function Profile({ user, onSelect }) {
  const label = `${user.first} ${user.last}`;
  const handle = () => onSelect(user.id);
  return <Row label={label} onSelect={handle} />;
}

const Row = memo(function Row({ label, onSelect }) {
  return <button onClick={onSelect}>{label}</button>;
});

go deeper

for a junior

Be able to say plainly that the React Compiler is a build-time plugin that adds memoization for you, so new code usually needs no useMemo or useCallback. Naming babel-plugin-react-compiler is a plus.

for a middle

Explain the mechanism: per-instance cache slots, dependencies inferred from the code, and reuse of the returned JSX element objects so React can bail out of subtrees. Contrast that granularity with hand-written dependency arrays that drift.

for a senior

Show you would verify rather than assume — DevTools' compiled badge plus a before/after profile of a real interaction — and be honest that a well-composed app may gain little because there was little wasted rendering to remove.

for a principal

Frame it as a policy shift: memoization stops being a per-PR judgment call and becomes a build guarantee, which is only worth taking if the codebase can hold the Rules of React as an enforced invariant. Own that tradeoff explicitly.

## What it is The React Compiler ships as `babel-plugin-react-compiler`, a Babel plugin you add to your build. "Compiler" here does not mean a new language: input is ordinary React JavaScript or TypeScript, output is ordinary JavaScript that imports a small cache helper from React. Nothing extra runs in the browser to watch your values; all the analysis happened at build time. It is an *optimising* compiler. Its contract is that compiled output renders exactly what the source rendered, only with less recomputation. It never changes semantics — as long as the source obeys the Rules of React, which is the precondition the whole design rests on. ## What it emits For each component or hook it successfully compiles, the compiler allocates a fixed-size cache array that lives with the component instance and survives re-renders — think of it as an array of `useMemo` slots that the compiler sized and filled for you. Then it walks the data flow of the function body and guards each computation with a comparison of exactly the values that computation reads. Take a component written with no memoization at all: ```jsx function Profile({ user, onSelect }) { const label = `${user.first} ${user.last}`; const handle = () => onSelect(user.id); return <Row label={label} onSelect={handle} />; } ``` Conceptually the compiled version does this on every render: if `user` is the same reference as last render, reuse the cached `label`; if `onSelect` and `user` are unchanged, reuse the cached `handle` function; if `label` and `handle` are unchanged, reuse the cached `<Row />` element object itself. Comparisons use `Object.is`, the same sameness check React uses everywhere else. The last step is the one people underrate. Returning the *same element object* as the previous render lets React's reconciler skip re-rendering that child subtree entirely — the bailout you would otherwise buy by wrapping `Row` in `React.memo`. The compiler gets it without any wrapper, and it gets it per-element rather than per-component. ## Why it beats hand memoization Three reasons. First, granularity. A human writes `useMemo` around the one value that looked expensive. The compiler memoizes at expression granularity across the whole function, including the JSX, and can split one component's output into several independently cached pieces. Second, correctness of dependencies. Hand-written dependency arrays drift: someone adds a variable to the body and forgets the array, and you get a stale value. The compiler derives the dependencies from the code, so they cannot fall out of sync with the body. Third, it memoizes the boring cases nobody bothers with. Most needless re-rendering in a real app is not one expensive calculation; it is a cascade of cheap components re-rendering because a parent produced fresh object and function props. The compiler removes that class wholesale. ## What becomes redundant In compiled components: `useMemo` for derived values, `useCallback` for handler identity, and `React.memo` wrappers whose only job was to stop parent-driven re-renders. Existing manual memoization is not an error — the compiler understands and preserves those calls — but it is noise you can delete as you touch files, rather than in one sweep. What does **not** become redundant is anything you used a memo hook for that was never about render cost, and any cost that is not "React re-ran a pure function for nothing": fetching, bundle size, or rendering an enormous number of DOM nodes. ## Configuration you should be able to name The plugin's `compilationMode` option controls how much it takes on. The default (`'infer'`) compiles the components and hooks it can recognise and prove safe; `'annotation'` compiles only functions you explicitly mark with a `"use memo"` directive, which is the usual way to pilot it on a subset of a codebase. Individual functions opt out with a `"use no memo"` directive. The React 19 baseline needs no extra runtime; older React versions are supported through the plugin's `target` option together with the `react-compiler-runtime` package. ## How you verify it Two checks. React DevTools marks compiled components, so you can confirm the plugin actually applied rather than silently skipping everything. And you measure: profile a real interaction before and after, because the compiler's payoff is proportional to how much wasted rendering you had, and a well-composed app may show very little.

  • If the compiler already stabilises identities, does React.memo do anything at all in a compiled app?
    Very little for compiled children, because the parent now hands them the same element object when nothing changed, so React bails out before `React.memo` would be consulted. It still matters at the seams: a child that comes from an uncompiled package, or one rendered by code the compiler skipped. Leaving an existing `React.memo` in place is harmless — it is redundant, not wrong.
  • Does the compiler make the app re-render less, or just make each render cheaper?
    Less. It does make individual renders cheaper by reusing derived values, but the bigger effect is structural: by returning the same element objects, it stops the parent-render cascade so whole subtrees never re-render. State updates still schedule a render of the owning component — the compiler prunes what that render propagates to, not whether it happens.
  • How would you confirm the compiler is actually applied to a given component?
    React DevTools badges compiled components, so open the component in the tree and look for that marker rather than trusting the build config. Complement it with a before/after profile of a real interaction — if the plugin silently skipped everything, the timings will be identical.

saying these in an interview costs you the question

  • Thinks the compiler runs in the browser and tracks values at runtime
  • Says it works like signals, subscribing to fine-grained reactive state
  • Claims it eliminates re-renders entirely, including state updates
  • Believes it rewrites components into classes or adds shouldComponentUpdate
  • Assumes it speeds up data fetching or shrinks the bundle

context