Memoization Heuristics — When useMemo/useCallback/memo Help vs Hurt
Memoization in React is not free — it costs a dep-array comparison every render, plus the memory to hold the cached value. The wins are real but narrow. useMemo / useCallback help when the value/function is expensive to compute or needs referential stability for a memo’d child or useEffect dep. React.memo helps when the component is expensive to render and its props are referentially stable. Outside those cases, blanket memoization adds complexity without measurable benefit and can hurt by preventing GC of cached values.
The mature rule: profile, then memoize. Don’t reach for useMemo/useCallback reflexively.
What does useMemo actually do?
Caches the result of a function across renders, recomputing only when a dep changes.
// toSorted() copies; items.sort() would mutate the prop in place
const sortedItems = useMemo(() => items.toSorted(comparator), [items, comparator]);Each render, React shallow-compares the deps array to the previous render’s. Same → returns cached value; different → recomputes.
Cost: the comparison (cheap), the memory to hold the cached value, the slight risk of stale values if deps are wrong.
Benefit: skip recomputation. Worth it when the function is meaningfully expensive (sort/filter big array, parse big string, compute derived data).
When does useMemo not help?
- Cheap computations —
useMemo(() => a + b, [a, b])costs more (comparison + memo) than justa + b. - Object/array creation alone —
useMemo(() => ({ x, y }), [x, y])is fine for downstream stability, but doesn’t itself “save” anything until a downstreammemo/effect uses the stability. - Deps array unstable — if a dep is a fresh object/function every parent render, the memo never hits the cache. Dead weight.
The pattern that often surprises newcomers: useMemo doesn’t make code faster by itself. It makes code faster by being skipped when deps are unchanged. If deps are unstable, no benefit.
When useCallback?
Same heuristics as useMemo, but for functions:
const onClick = useCallback(() => save(id), [id]);Use when:
- Passing the function to a
memo’d child that would otherwise re-render due to a new function reference. - Passing the function to
useEffect’s dep array so the effect doesn’t re-fire every render. - Passing the function to a hook that caches by reference (e.g., custom hook subscribing to events).
Don’t bother for:
- Functions passed to regular (non-
memo’d) children that re-render anyway. - Top-level event handlers (
onClick,onChange) where the receiving element doesn’t care about reference stability.
When does React.memo help?
Required conditions:
- The component is expensive to render (large tree, heavy DOM, or compute-heavy logic).
- Parent re-renders more often than the props change — without
memo, the component re-renders on every parent render. - Props are referentially stable — same identity when “the same” — or you provide a custom equality.
const ExpensiveRow = React.memo(function ExpensiveRow({ item, onSelect }) {
return <div>{/* heavy rendering */}</div>;
});For this to actually skip renders, the parent must pass the same item and onSelect references (use useMemo/useCallback upstream).
When does React.memo hurt?
Three modes:
- Cheap component + unstable props — every render pays the shallow comparison cost and re-renders anyway. Net negative.
- Complex props that need a custom comparator —
memo(C, (a, b) => deepEqual(a, b.props))— the comparator itself can cost more than the render. - Components where props change every render anyway —
memois useless dead code; readers wonder why it’s there.
What’s “premature memoization”?
Sprinkling useMemo/useCallback/memo across the codebase under the belief it’s “always a good idea.” It’s not — each adds a small cost (comparison, GC pressure) and, more importantly, adds complexity that the next reader has to reason about.
Mature codebases pick memoization deliberately:
- After profiling shows a hot spot.
- When passing values across a
memoboundary. - When stabilizing context values or
useEffectdeps.
Outside those, it’s noise.
How would you measure whether a useMemo is helping?
- React Profiler with “why did this render?” enabled — see which components actually re-rendered and what triggered them.
- Compare renders with and without the memoization. Often the saved time is small.
<Profiler>API —baseDuration - actualDurationis the memoization savings.
If the answer is “0.5ms saved on a 30ms commit,” remove it — the complexity isn’t worth it.
What’s the React Compiler (formerly React Forget)?
A compile-time optimizer, stable since React Compiler 1.0 (October 2025), that automatically memoizes: it analyses your components and inserts the equivalent of useMemo/useCallback/memo where they help. With it enabled, new code skips manual memoization; it bails out of components that break the rules of React (mutating props or state, as items.sort() above does), which is one more reason to write pure render code. Understanding why memoization helps (or doesn’t) is still the senior insight.
What’s useMemo for referential stability — concrete example.
function Dashboard({ items }) {
// Without useMemo — new object every render
const config = { sortBy: "date", filter: "active" };
return <Chart items={items} config={config} />;
}
const Chart = React.memo(function Chart({ items, config }) {
// expensive...
});Chart re-renders on every Dashboard re-render because config is a new object every time. Fix:
function Dashboard({ items }) {
const config = useMemo(() => ({ sortBy: "date", filter: "active" }), []);
return <Chart items={items} config={config} />;
}Now Chart only re-renders when items change. The useMemo here isn’t saving compute — it’s enabling memo to actually skip renders.
(Or hoist config outside the component as a const, which is cheaper. Hoisting trumps memoization when the value is truly constant.)
Context value memoization.
Every Context Provider value change re-renders all consumers. A non-memoized inline object defeats Context entirely:
// Bad — value identity changes every render → all consumers re-render
<MyContext.Provider value={{ user, setUser, settings }}>
// Better
const value = useMemo(() => ({ user, setUser, settings }), [user, settings]);
<MyContext.Provider value={value}>Even better at scale: split the context so consumers only subscribe to what they need.
Are useMemo/useCallback only React?
The pattern is React-specific because of React’s re-render-by-default model. Vue’s reactivity is per-property — components only re-render when their reactive deps change, so most of this discussion doesn’t apply. Vue has v-memo and shallowRef for the rare cases — see Vue Performance — v-memo, shallowRef, markRaw, defineAsyncComponent. Svelte’s compile-time tracking sidesteps it entirely.
Gotchas
useMemois not guaranteed — React docs explicitly say the cache may be dropped to free memory. Don’t rely on it for correctness.- Empty deps
[]— value computed once for the component’s lifetime. Useful for constants; misleading if the function actually depends on something. - Stale closures in memoized callbacks —
useCallback(() => fn(stateAtMount), [])captures the mount-time value. Add proper deps. memo+ children prop — children is a new reference every render unless wrapped or passed as a stable function.memorarely helps for layout components that takechildren.useReducerinstead ofuseStatewhen actions trigger many state updates — dispatchers are stable by default (nouseCallbackneeded).- Object spread in render —
{ ...props, foo: 1 }creates a new object; downstreammemowon’t help unless youuseMemothe result.
Related
Further reading
- React docs —
useMemo: https://react.dev/reference/react/useMemo - React docs —
useCallback: https://react.dev/reference/react/useCallback - React docs —
memo: https://react.dev/reference/react/memo - Kent C. Dodds — “When to useMemo and useCallback”: https://kentcdodds.com/blog/usememo-and-usecallback
- React Compiler (React Forget) docs: https://react.dev/learn/react-compiler
Interview angle 5
- “When should you memoize?” - for one of three reasons, and you should be
able to name which: an expensive computation, referential stability for a
memoboundary, or referential stability for an effect dependency. “It felt like it might help” is not one of them. - “What does memoization cost?” - a dependency comparison on every render
plus retained memory for the cached value, and for
React.memoa shallow prop comparison per render. On cheap components that is a net loss, which is why blanket memoization can measurably slow an app down. - “How do you decide what to memoize?” - profile first. The DevTools
Profiler with “why did this render” names the unstable prop or context value,
and the fix is usually one
useMemoat the source rather thanmemoon every consumer. - “Why does context need memoizing?” - because every consumer re-renders when the value’s identity changes, and an object literal in the provider is a new identity on every parent render. Memoize it, and past a certain size split the context by domain so unrelated changes do not ripple.
- “Does the React Compiler change this advice?” - yes, substantially.
React Compiler 1.0 has been stable since 2025-10 and auto-memoizes, so in a
compiled codebase hand-written
useMemoanduseCallbackare mostly noise that makes the code harder to read without making it faster.- Follow-up: “So is the mental model obsolete?” - no. You still need it to read a profile, to understand what the compiler did and did not memoize, and to work in the code that has not adopted it. The compiler changes what you write, not what you need to understand.