I deleted every useMemo and nothing happened
There is a category of React code that exists only to stop React from doing work it would otherwise do. useMemo around a derived array. useCallback around a handler that gets passed to a memoised child. 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 has to be maintained, and every dependency array is a small bug waiting for a refactor.
The React Compiler is the project that promised to make that code unnecessary. It went stable in late 2025, it is the default in new Next.js 16 projects, and it works by doing at build time what the hooks did by hand: it looks at each component, figures out which values can change between renders, and caches everything else. The claim is that a component written in plain React, with no memoization, comes out of the compiler as fast as or faster than the hand tuned version.
I had a production app with 212 of those hooks in it. I turned on the compiler and deleted them all to see what the claim was worth.
The app and the method
A dashboard, about 180 components, React 19, Next.js 16, TanStack Query for data. Heavy tables, a few charts, forms with a lot of controlled inputs. It had been profiled and tuned over years, which is why it had 212 memoization hooks and 31 React.memo wrappers. It was not slow. The question was whether it would stay not slow without them.
The method was blunt. Enable the compiler:
// next.config.ts
export default {
reactCompiler: true,
};Run the app, check that nothing broke. Then remove every useMemo, useCallback and React.memo with a codemod, run the app again, and measure. I used the React DevTools profiler on the five interactions that had historically been the slow ones: opening the main table with 2,000 rows, sorting it, typing into the filter box, opening the row detail drawer, and switching between date ranges on the charts. Each measured ten times before and after, on the same machine, with the same data.
The result
| Interaction | With hooks | Compiler, no hooks |
|---|---|---|
| Open 2,000 row table | 142 ms | 138 ms |
| Sort table | 61 ms | 59 ms |
| Type one character in filter | 18 ms | 17 ms |
| Open detail drawer | 34 ms | 36 ms |
| Switch chart range | 88 ms | 84 ms |
All within noise. I stared at those numbers for a while, because I had expected at least one of them to regress. Two thousand table rows with a handler on every cell was the case I was sure would fall over without useCallback. It did not, because the compiler memoised the handler for me, the same way I had, and it did it correctly, which is more than I could say for two of the manual versions.
The number that did change was in 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 is not magic and it is worth knowing what it is actually doing so the exceptions make sense.
The compiler rewrites each component and hook into a form where every expression is cached in a slot and recomputed only when its inputs change. It infers the inputs by analysing the code, which is what your dependency array was doing by hand, except the compiler does not forget a dependency and does not include one that is not needed. It applies this to values, to JSX, and to functions, so a handler defined inline is stable across renders as long as the things it closes over are stable.
For that analysis to be sound the component has to follow the rules of React: no mutation of props or state, no reads of refs during render, pure render functions. If the compiler sees a component that breaks a rule, it skips that component entirely and leaves it as written. It does not try to be clever about code it cannot prove safe. That skipping is silent by default, which leads to the first exception.
Exception one: the components it refused
The compiler skipped nine of the 180 components. There is an ESLint plugin, eslint-plugin-react-compiler, that reports the reason for each skip, and the reasons were all legitimate.
Four components mutated an object from props to add a computed field before rendering it. That is a rules violation that had never caused a visible bug because the object came from a query result and nobody else read it after. The fix was to derive a new object instead.
Three read ref.current during render to decide what to draw. Two of them were legacy code from before useSyncExternalStore existed and moved to it. One was a genuine measure-then-render pattern and moved to useLayoutEffect with state.
Two called a hook conditionally in a way that was technically fine because the condition was constant but that the compiler could not prove. Those got restructured.
After the fixes the compiler handled all 180. The point is that the skips were not the compiler failing. They were nine places where the code had been breaking the rules for years and the manual hooks were compensating for it.
Exception two: the memo that was doing something else
Of the 212 hooks, four turned out to be load bearing in a way that had nothing to do with rendering speed.
Two useMemo calls were creating objects that were then used as keys in a WeakMap cache elsewhere. The memo was keeping the object identity stable so the cache would hit. The compiler also keeps it stable, as it happens, but that is an implementation detail, and relying on it for correctness is wrong in the same way relying on useMemo for correctness was always wrong. The React docs have said for years that useMemo is a performance hint and may be dropped. Those two became explicit, with the key stored in a ref.
One useCallback was being passed to a third party library that registered it as an event listener on mount and never re-registered. Without a stable reference the listener was the first render's closure forever. The compiler keeps the reference stable too, so nothing broke, but again, the code was depending on an optimisation. It got an explicit ref.
One React.memo was on a component that received a new inline object as a prop on every parent render and used a custom comparison function to deep compare it. That is a real case: the compiler memoises based on identity, not on structure, and if the parent creates a new object each time, the child re-renders each time. The fix there was on the parent, which now creates the object once. The memo with the custom comparator was deleted after that.
So four out of 212 were doing something the compiler does not promise to do, and in every one of them the right change was to stop depending on memoization for correctness. The other 208 were doing exactly what the compiler does, by hand, less reliably.
Exception three: the expensive computation
There is one thing the compiler does not do, and it is the case useMemo was originally invented for. If a component computes something expensive from its props, the compiler will cache the result and skip the computation when the props have not changed. That is the same as useMemo. But if the computation is expensive and the props do change on every render for a legitimate reason, neither the compiler nor useMemo helps, and the fix is to move 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, and the series was a new array from the query on every refetch, every 30 seconds. The memo had been hiding that the aggregation should happen in the query's select function, once per fetch, not in the component. That moved, the component got simpler, and it 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 is no reason to write useMemo or useCallback at all, and if you find yourself reaching for one, the question to ask is what you are 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 regardless. 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.
The four exceptions I found are the ones to watch for: identity used as a cache key, a reference captured by something that never re-reads it, structural comparison in a memo, and expensive work that belongs in the data layer. Each one is a place where the hook was doing more than a performance hint, and each one is better written explicitly.
The React team said the compiler would let us stop thinking about memoization. In my app it let me stop thinking about it and also showed me the eight or nine places where I had been thinking about it wrong. That was the better outcome.