React

Turning the React Compiler on by default across new projects

RCT

Last year we tested the React Compiler cautiously, project by project, watching closely for the subtle correctness issues that any automatic memoization tool risks introducing. A year of real production usage across several of those pilots gave us enough confidence to make it the default for every new React project going forward, rather than continuing to evaluate it case by case.

Manual memoization has become something we write only for the rare case the compiler's bailout diagnostics flag, rather than a habit applied everywhere by default the way it used to be. That shift changes the shape of a typical pull request review: instead of checking whether a component correctly wraps its callbacks and computed values in the right memoization hooks, reviewers now mostly just check whether the compiler is actually able to optimize the component at all, and if it isn't, why not. The compiler's bailout diagnostics turned out to be more useful than we expected going in, since a component the compiler can't safely optimize is often a component with a pattern worth reconsidering anyway, an unstable prop reference, a side effect hiding somewhere it shouldn't be.

A bailout looks like this in the compiler's output, pointing directly at the pattern it couldn't safely reason about:

Compiling ProductFilters (src/components/ProductFilters.jsx)

  ⚠ Bailed out of optimizing this component
  Reason: found a mutation of a variable captured from an outer scope
  Location: src/components/ProductFilters.jsx:14:6

    12 |   function handleChange(value) {
    13 |     lastValue = value; // mutates outer-scope variable
    14 |     setFilters(f => ({ ...f, query: value }));
    15 |   }

That specific diagnostic pointed at a stray outer-scope variable a previous engineer had used to sidestep a stale-closure bug years earlier, a workaround that had outlived the bug it was written for. Fixing the actual pattern, rather than suppressing the warning, let the compiler optimize the component and removed a subtle footgun nobody had gotten around to cleaning up.

The mental overhead of thinking about re-renders has dropped noticeably across the team. Junior engineers in particular used to spend real energy learning when and how to reach for `useMemo` and `useCallback` correctly, a skill that took months to build real intuition for and that experienced engineers sometimes disagreed about in code review. That entire category of discussion has mostly gone quiet.

We measured actual re-render counts on one of the pilot dashboards before making the compiler the default everywhere, since "components should re-render less" is easy to assert and worth checking directly rather than trusting the general narrative around the compiler:

Component            re-renders/min (manual memo)   re-renders/min (compiler)
FilterPanel           142                             38
ProductGrid           89                              91
OrderSummary          204                             52

ProductGrid barely moved, which made sense once we looked at it: it had already been carefully, correctly memoized by hand, so the compiler had little room to improve on it. FilterPanel and OrderSummary, both places where manual memoization had been inconsistent or slightly wrong, saw the compiler catch optimization opportunities a human reviewer had missed.

It hasn't disappeared completely, there are still edge cases the compiler can't safely handle and a developer needs to recognize those cases, but the frequency of that kind of conversation dropped enough that it freed up real review time for discussing actual application logic instead. A typical component today just writes the obvious code and lets the compiler handle the rest:

function ProductList({ products, filterText, onSelect }) {
  const filtered = products.filter((p) =>
    p.name.toLowerCase().includes(filterText.toLowerCase())
  );

  const handleSelect = (id) => onSelect(id);

  return (
    <ul>
      {filtered.map((p) => (
        <ProductRow key={p.id} product={p} onSelect={handleSelect} />
      ))}
    </ul>
  );
}

Migrating existing projects onto the compiler took more care than starting fresh with it. A codebase with years of manual memoization already in place needed a careful pass to identify memoization that had become redundant under the compiler versus memoization that was still doing something the compiler couldn't infer on its own, usually around expensive computations gated behind conditions the compiler couldn't statically verify were safe to skip. We didn't strip out every existing `useMemo` call reflexively; a few were left in place specifically because removing them and trusting the compiler to infer the same optimization introduced a small but measurable performance regression in profiling.

Most of the migration looked like straightforward deletion, though, once we'd verified the compiler could actually see through a given case. A typical cleanup removed manual memoization the compiler now handles on its own:

- function ProductRow({ product, onSelect }) {
-   const handleClick = useCallback(() => onSelect(product.id), [product.id, onSelect]);
-   const formattedPrice = useMemo(() => formatCurrency(product.price), [product.price]);
-
-   return <li onClick={handleClick}>{product.name} — {formattedPrice}</li>;
- }
+ function ProductRow({ product, onSelect }) {
+   const handleClick = () => onSelect(product.id);
+   const formattedPrice = formatCurrency(product.price);
+
+   return <li onClick={handleClick}>{product.name} — {formattedPrice}</li>;
+ }

The compiler produces the equivalent memoized output at build time, so the runtime behavior is unchanged, what changed is that a human no longer has to get the dependency array right by hand, which was reliably the single most common source of subtle bugs in code review under the old approach.

The honest caveat is that the compiler doesn't remove the need to understand what re-rendering actually costs and why. It removes the need to manually annotate that understanding into every component by hand, but a developer who doesn't understand why a re-render might be expensive in the first place will still write code that performs poorly, they'll just do it without the compiler's bailout diagnostics giving them a clear signal to investigate. We still spend real time in onboarding explaining the underlying rendering model, the compiler changed what engineers write day to day, not what they need to understand to write it well.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients