Skip to content

React Compiler in 2026: What Happened When I Stopped Memoizing

React Compiler in 2026: What Happened When I Stopped Memoizing

Short version for the impatient: turn React Compiler on, delete most of your useMemo and useCallback calls, and budget your afternoon for the build config rather than the code. If you want the details, keep reading.

I put this off for months after the compiler went stable, and not for a good reason. The reason was that I had sprinkled memo hooks across a mid-size dashboard app like seasoning, over about two years, mostly in response to profiler sessions I only half remember. The idea of a compiler quietly rewriting all of that made me nervous in a way I couldn’t defend out loud. Then I finally sat down with it on a Saturday. The code changes took about an hour. The build tooling took the rest of the day, which I think is the actual story here.

What the compiler actually does

React Compiler is a build step. It reads your components, works out which values can be cached between renders, and inserts the memoization for you at a finer grain than you would ever bother to write by hand. The React team shipped version 1.0 in October 2025 after running it inside Meta for a long stretch, and they reported gains in the Meta Quest Store of roughly 12% on initial load and more than 2.5x on interactions.

Two things about those numbers. They are Meta’s numbers on Meta’s app, so treat them as a ceiling rather than a promise. And “2.5x faster interactions” in a heavily interactive 3D storefront is a different animal from a CRUD dashboard where the slow part is a 400ms API call. My own app got faster, but the honest measurement is “a few frames of jank disappeared from the table filters,” not a number I would put on a slide.

The thing I didn’t expect: the win was mostly about deletion. My components got shorter. Dependency arrays, which are the single most common source of stale-closure bugs I have shipped, mostly went away.

The before and after

Here is a trimmed version of a filter panel from that dashboard. React 18 style, written by a slightly paranoid version of me:

function FilterPanel({ rows, filters, onChange }) {
  const visible = useMemo(
    () => rows.filter((r) => matches(r, filters)),
    [rows, filters]
  );

  const totals = useMemo(
    () => ({
      count: visible.length,
      revenue: visible.reduce((sum, r) => sum + r.amount, 0),
    }),
    [visible]
  );

  const handleReset = useCallback(() => {
    onChange(DEFAULT_FILTERS);
  }, [onChange]);

  const handleToggle = useCallback(
    (key) => onChange({ ...filters, [key]: !filters[key] }),
    [filters, onChange]
  );

  return (
    <Panel>
      <Summary totals={totals} />
      <Controls onToggle={handleToggle} onReset={handleReset} />
      <Table rows={visible} />
    </Panel>
  );
}

export default memo(FilterPanel);

And the same component with the compiler on:

function FilterPanel({ rows, filters, onChange }) {
  const visible = rows.filter((r) => matches(r, filters));

  const totals = {
    count: visible.length,
    revenue: visible.reduce((sum, r) => sum + r.amount, 0),
  };

  return (
    <Panel>
      <Summary totals={totals} />
      <Controls
        onToggle={(key) => onChange({ ...filters, [key]: !filters[key] })}
        onReset={() => onChange(DEFAULT_FILTERS)}
      />
      <Table rows={visible} />
    </Panel>
  );
}

export default FilterPanel;

That is the whole pitch. Same behaviour, same or better render counts, and about half the lines. Multiply that across sixty components and you feel it in code review more than in the profiler.

One caveat I hit immediately: matches and the reduce callback have to be pure for any of this to be safe. The compiler assumes you follow the rules of React. If you were mutating props somewhere deep in a helper, you had a bug before and now you have a bug that shows up at a different time, which is worse. I found two of these. Both were mine. Both were embarrassing.

Where I kept useMemo anyway

The hooks didn’t get deprecated, and the docs are clear that they still work as an escape hatch. I kept them in two places.

The first is any value that feeds a useEffect dependency array. The compiler decides what to cache based on its own analysis, and I don’t want an effect’s re-run behaviour to depend on that analysis. If a change to the caching heuristics would change how often my WebSocket reconnects, I want an explicit useMemo sitting there saying so. This is the case the React team calls out too, and I agree with it.

The second is genuinely expensive work that isn’t about render output at all: parsing a large CSV blob, building a search index, that kind of thing. The compiler is optimising re-renders. It isn’t a general purpose caching layer, and treating it like one will disappoint you.

Everything else went. Including a React.memo wrapper I had added in 2023 that, on inspection, was doing nothing at all because I was passing a fresh object literal as a prop one level up.

Installing it in 2026 is the annoying part

Here is where the Saturday went. The compiler ships as a Babel plugin, and it needs to run first in the plugin pipeline because it wants the original source before anything else transforms it. That constraint is fine on its own. The problem is that the JavaScript build world moved underneath it.

@vitejs/plugin-react v6 dropped Babel in favour of oxc for the default transform, which happened around the same window as the compiler going stable. So the familiar react({ babel: { plugins: [...] } }) incantation you’ll find in half the blog posts doesn’t work on current Vite. You add a separate Babel plugin to the chain instead. The installation guide covers the current setups, and it’s worth reading it rather than a tutorial, because this specific area has churned more than the compiler itself.

Next.js is easier. There is a reactCompiler flag in next.config.js and that’s roughly it. Expo bundles it too, with the lint rules on by default in recent SDKs.

If you’re adopting into an older codebase, the configuration reference has options for scoping the compiler to specific directories. I used that for a week to compile only src/features/ while I convinced myself nothing was on fire, then removed the restriction. That gradual approach cost me nothing and I would do it again.

The lint rule is quietly the best part

Compiler diagnostics now surface through eslint-plugin-react-hooks. If you had the separate eslint-plugin-react-compiler package installed, you can drop it.

I say this is the best part because the lint rule is useful even if you never enable the compiler. It tells you where your components break the rules of React: mutation during render, conditional hooks, refs read in the wrong phase. Those are real bugs, and they are the same class of bug that makes React behave strangely in ways people usually blame on React. I ran the lint pass across a project that isn’t on the compiler at all and it found four legitimate issues.

If your team is arguing about whether to adopt the compiler, adopt the lint rule first. It’s a much smaller conversation and you learn how compiler-ready your code actually is.

What this doesn’t fix

Automatic memoization makes renders cheaper. It doesn’t make your app fast if the slow part is elsewhere, and in most apps I have worked on, the slow part is elsewhere: a request waterfall, an unindexed query, a 900KB bundle of date-formatting library. I wrote about a version of this problem in my post on the N+1 query problem in Laravel, and the shape is identical on the frontend. Fix the waterfall before you optimise the render.

It also won’t save a component that re-renders because its parent re-creates the entire data array on every keystroke. The compiler works within the rules it can see. Bad data flow is still bad data flow.

What I would do this week

Add eslint-plugin-react-hooks with the compiler rules enabled and run it across your codebase. Do not change anything yet. Just read the output, because that list is a decent map of where your React is quietly weird.

If that comes back clean, or close to it, turn the compiler on for one directory, ship it, and watch your error tracker for a week. Then start deleting memo hooks, and delete them by hand rather than with a regex, because the ones you keep matter.

I build and maintain React and Laravel apps for a living, and you can see some of that work over on my portfolio. The short version of two years of this: most of the memoization I wrote was cargo cult, and I am glad a compiler took it away from me.