React Compiler Wasn't Memoizing Half My App — Here's How I Found Out
I enabled React Compiler last Tuesday and expected to delete half my useMemo calls by lunch. By Friday I was staring at React DevTools wondering why a list component with 200 items still re-rendered from scratch every time I typed a single character into a search box. The compiler was on. The build succeeded. No errors in the console. And yet the performance hadn't changed at all.
Here's what nobody mentions in the launch posts: React Compiler doesn't throw when it can't optimize your code. It just skips that component, silently, and moves on to the next one. No warning, no error, no console message. Your component runs exactly as you wrote it — un-memoized — and you only find out when something feels slow.
The badge that tells the truth
The tell is a small badge in React DevTools. When you open the Components tab, compiled components show a "Memo ✨" badge next to their name. No badge, no memoization. I opened DevTools, looked at my SearchableList component, and the badge was missing.
I went through the whole tree. Out of 14 components in that feature, 6 had the badge and 8 didn't. The compiler was skipping more than half my code, and I had no idea until I went looking.
Three patterns that triggered bailouts
React's docs describe a bailout as a safety feature — the compiler skips optimization when it can't statically prove your code follows the Rules of React, rather than risk changing your app's behavior. Fair enough. The bailouts I hit all fell into three buckets.
1. Side effects during render. I had a component that called new Date().toISOString() during render to timestamp a log row. The compiler can't cache a value that changes on every call, so it bailed on the entire component:
function LogRow({ message }) {
const timestamp = new Date().toISOString() // side effect in render
return <li>{timestamp} — {message}</li>
}
The fix was moving the timestamp into a useEffect and reading it from state. Not glamorous, but the badge came back.
2. An incomplete useMemo dependency array. This one stung. I had a useMemo that filtered a list but left the filter function out of the dependency array — a stale-closure bug I'd been ignoring for months because it "worked":
const filtered = useMemo(
() => items.filter(item => item.matches(activeFilter)),
[items] // activeFilter is missing — compiler bails
)
The compiler saw the missing dependency, couldn't safely extend my memoization, and skipped the whole component. Honestly? The compiler caught a bug I'd been pretending didn't exist. I added activeFilter to the array and the badge came back.
3. An effect depending on object identity. My search box passed a combined state object to a child, and the child had a useEffect that depended on that object's reference. Because the compiler might memoize the object differently than my manual code did, it bailed to avoid causing the effect to over-fire or loop:
// Parent creates a new object every render
const searchState = { query, filters, page }
// Child's effect fires whenever the reference changes
useEffect(() => {
performSearch(searchState)
}, [searchState]) // identity-dependent — compiler bails
The fix was depending on the primitive values directly instead of the wrapper object:
useEffect(() => {
performSearch({ query, filters, page })
}, [query, filters, page])
Sometimes the old ways are the best ways.
The one directive that saved my sanity
When I wasn't sure whether a bug was caused by the compiler or was already living in my code, I dropped a "use no memo" directive at the top of the component body:
function SearchableList({ items }) {
"use no memo" // temporarily disable compilation
// ... rest of component
}
If the bug disappeared with compilation off, the compiler had exposed a Rules of React violation that my manual memoization was papering over. If the bug stayed, it was mine and the compiler was innocent. I ran this on three components, found two real violations, and filed the third as "works as intended, I guess."
If you're juggling a lot of these debugging snippets like I was, keeping them one shortcut away beats digging through old commits — I started dumping bailout checklists and fix patterns into Snippet Ark so I'm not re-deriving them every time a badge goes missing.
What I actually deleted
After two days of this, I removed 11 useMemo calls and 7 useCallback calls that the compiler now handles automatically. I kept 3 useMemo calls where the memoized value was consumed as an effect dependency and I needed explicit control over when that effect re-fired — the one case where manual memoization still earns its keep, according to React's own docs.
The compiler can also memoize in places manual hooks can't reach, like values computed after an early conditional return. That alone cleaned up two components I'd written off as "too awkward to optimize."
If you're about to flip the compiler on, don't do what I did and assume it just works. Open DevTools, check for the badge, and "use no memo" anything suspicious. The compiler is smart. Your code might not be.