Skip to content

I deleted every useMemo and nothing happened

The React Compiler is stable, on by default in new Next.js apps, and supposed to make most manual memoization redundant. I removed 212 useMemo and useCallback calls from a production app to see if that was true. It was, with four exceptions, and the exceptions show what those hooks were really doing.

Ahmet ZeybekMarch 10, 20268 min read10 min listen

listen10 min

I deleted every useMemo and nothing happened

0:00
10:07

I deleted every useMemo and nothing happened

Some React code exists only to stop React from doing work it would otherwise do: a useMemo around a derived array, a useCallback around a handler passed to a memoised child, a React.memo around a component that re-rendered too often in a profiler session three years ago. None of it is business logic. All of it needs maintaining, and every dependency array is a small bug waiting for the next refactor.

The React Compiler is the project that promised to make that code unnecessary.1 It does at build time what the hooks did by hand: it looks at each component, works out which values can change between renders, and caches the rest. The claim is that a component written in plain React with no memoization comes out of the compiler as fast as the hand tuned version, or faster.

I had a production app with 212 of those hooks in it, so I turned the compiler on and deleted them all to see what the claim was worth.

The app and the method

It's a dashboard with about 180 components, on React 19 and Next.js 16, with TanStack Query for data: heavy tables, a few charts, forms with a lot of controlled inputs. It had been profiled and tuned for years, which is how it ended up with 212 memoization hooks and 31 React.memo wrappers. It wasn't slow. The question was whether it would stay that way without them.

The method was blunt. Turn on the compiler:

// next.config.ts
export default {
  reactCompiler: true,
};

Run the app and check nothing broke. Then strip every useMemo, useCallback and React.memo with a codemod, run it again, and measure. I used the React DevTools profiler on the five interactions that had always been the slow ones: opening the main table with 2,000 rows, sorting it, typing in the filter box, opening the row detail drawer, and switching date ranges on the charts.2

The result

InteractionWith hooksCompiler, no hooks
Open 2,000 row table142 ms138 ms
Sort table61 ms59 ms
Type one character in filter18 ms17 ms
Open detail drawer34 ms36 ms
Switch chart range88 ms84 ms

All of it is within noise. I stared at those numbers for a while, because I'd expected at least one to get worse. Two thousand table rows with a handler on every cell was the case I was sure would fall over without useCallback. It didn't, because the compiler memoised the handler the same way I had, and got it right, which is more than I can say for two of the manual versions.

The number that did change was the size of the codebase. Removing the hooks deleted about 1,400 lines, including the dependency arrays and the comments explaining why a particular dependency was left out on purpose.

What the compiler does, briefly

It isn't magic, and it helps to know what it actually does so the exceptions make sense.

The compiler rewrites each component and hook so that every expression is cached in a slot and only recomputed when its inputs change. It works out the inputs by analysing the code, which is what your dependency array did by hand, except the compiler never forgets a dependency or adds one that isn't needed. It does this for values, for JSX and for functions, so a handler defined inline stays stable across renders as long as whatever it closes over is stable.

For that analysis to hold up, the component has to follow the rules of React: no mutating props or state, no reading refs during render, pure render functions. When the compiler sees a component break a rule, it skips that component completely and leaves it as written. It doesn't try anything clever with code it can't prove safe. By default it skips silently, which brings me to the first exception.

Exception one: the components it refused

The compiler skipped nine of the 180 components. An ESLint plugin, eslint-plugin-react-compiler, reports the reason for each skip, and every reason was legitimate.

Four components mutated an object from props to add a computed field before rendering it. That breaks the rules.3 The fix was to derive a new object.

Three read ref.current during render to decide what to draw. Two were legacy code from before useSyncExternalStore existed, and moved to it. One was a real measure-then-render pattern, and it moved to useLayoutEffect with state.

Two called a hook conditionally in a way that was technically fine, because the condition never changed, but the compiler couldn't prove that. Those got restructured.

After the fixes the compiler handled all 180. The skips weren't the compiler failing. They were nine places where the code had been breaking the rules for years, with the manual hooks covering for it.

Exception two: the memo that was doing something else

Four of the 212 hooks turned out to matter in a way that had nothing to do with rendering speed.

Two useMemo calls created objects that were used as keys in a WeakMap cache somewhere else. The memo kept the object identity stable so the cache would hit. The compiler happens to keep it stable too, but that's an implementation detail, and relying on it for correctness is wrong for the same reason relying on useMemo for correctness always was.4 Those two became explicit, with the key stored in a ref.

One useCallback was handed to a third party library that registered it as an event listener on mount and never registered it again. Without a stable reference, the listener would have been the first render's closure forever. The compiler keeps the reference stable as well, so nothing broke, but once again the code depended on an optimisation, so it got an explicit ref.

One React.memo sat on a component that got a new inline object as a prop on every parent render, with a custom comparison function to deep compare it. That's a real case. The compiler memoises on identity, not on structure, so if the parent makes a new object every time, the child re-renders every time. The fix went in the parent, which now creates the object once, and after that the memo with its custom comparator was deleted.

So four of 212 were doing something the compiler doesn't promise to do, and every time the right change was to stop depending on memoization for correctness. The other 208 did exactly what the compiler does, by hand and less reliably.

Exception three: the expensive computation

There's one thing the compiler doesn't do, and it's the case useMemo was invented for in the first place. If a component computes something expensive from its props, the compiler caches the result and skips the work when the props haven't changed, just like useMemo. But if the computation is expensive and the props really do change on every render, neither one helps, and the fix is to do the work somewhere else.

I had one of those: a chart component that ran a 40 ms aggregation over the raw series on every render, memoised on the series, where the series was a new array from the query on every refetch, every 30 seconds. The memo had hidden the fact that the aggregation belonged in the query's select function, once per fetch, and not in the component. I moved it, the component got simpler, and that was the only change in the whole exercise that made something measurably faster.

What I would tell someone starting today

Turn the compiler on before you write a single hook. In a new project there's no reason to write useMemo or useCallback at all. If you catch yourself reaching for one, ask what you're actually trying to keep stable, and why.

In an existing project, turn it on, run the ESLint plugin, and fix the components it skips. Those fixes are worth doing anyway. Then delete the hooks in a separate commit so the diff can be reviewed, and profile the interactions you care about before and after. Expect flat numbers.

Watch for the exceptions I found: identity used as a cache key, a reference captured by something that never reads it again, structural comparison in a memo, and expensive work that belongs in the data layer. In each of those the hook was doing more than hinting at performance, and each is better written out explicitly.

The React team said the compiler would let us stop thinking about memoization. In my app it did that, and it also showed me the eight or nine places where I'd been thinking about it wrong, which I got more out of.

Footnotes

  1. It went stable in late 2025, and it's the default in new Next.js 16 projects. ↩

  2. Each one was measured ten times before and after, on the same machine with the same data. ↩

  3. It had never caused a visible bug because the object came from a query result and nothing else read it afterwards. ↩

  4. The React docs have said for years that useMemo is a performance hint that may be dropped. ↩

questions this post answers

Does the React compiler really make memoization hooks unnecessary?

Yes, for 208 of 212. The compiler infers dependencies correctly and memoises values and functions the same way the hooks did. The numbers across five interactions stayed flat when all hooks were deleted. Dependencies are gone, one line per hook, so bugs in arrays disappear.

What did the four exceptions have in common?

None of them was about rendering speed. Two useMemo calls were holding an object identity stable so a WeakMap cache elsewhere would hit, and one useCallback went to a library that registered it on mount and never re-registered. Those three became explicit refs. The fourth, a React.memo with a custom comparator, was fixed on the parent, which now creates the object once.

Why did nine components make the compiler refuse to work?

Four mutated props. Three read ref.current during render. Two called hooks conditionally. The compiler doesn't try to be clever about breaking the rules. Those nine were breaking the rules for years and the manual hooks were compensating. Fixing them made the compiler work on all 180.

What's the one usememo case the compiler can't help with?

Expensive computation from props when the props change on every render for a legitimate reason. A chart aggregating 40ms of data on every refetch every 30 seconds. The memo was hiding that the aggregation belonged in the query's select function, not the component.

What happens when you turn the compiler on in an existing app?

Turn it on, run the ESLint plugin, and fix the components it skips. Here that was nine places where the code had been breaking the rules of React for years and the hooks were compensating. Then delete the hooks in a separate commit so the diff is reviewable, and profile the interactions you care about before and after. Expect the numbers to be flat.