Memoization Heuristics — When useMemo/useCallback/memo Help vs Hurt
TL;DR
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.
Interview Q&A
Q: What does useMemo actually do?
A: Caches the result of a function across renders, recomputing only when a dep changes.
const sortedItems = useMemo(() => items.sort(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).
Q: When does useMemo not help?
A:
- 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.
Q: When useCallback?
A: 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.
Q: When does React.memo help?
A: 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).
Q: When does React.memo hurt?
A: 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.
Q: What’s “premature memoization”?
A: 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.
Q: How would you measure whether a useMemo is helping?
A:
- 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.
Q: What’s the React Compiler (formerly React Forget)?
A: A compile-time optimizer (still rolling out) that automatically memoizes — analyzes your code and inserts useMemo/useCallback/memo equivalents where they’d help. The promise: write idiomatic React without manual memoization, the compiler handles it.
Once stable, much of the manual memoization discussion fades. Until then, it’s the developer’s job. Even after, understanding why memoization helps (or doesn’t) is the senior insight.
Q: What’s useMemo for referential stability — concrete example.
A:
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.)
Q: Context value memoization.
A: 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.
Q: Are useMemo/useCallback only React?
A: 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 ../06_vue/13_performance.md. Svelte’s compile-time tracking sidesteps it entirely.
Gotchas / edge cases
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.
What a senior is expected to say
- “Memoization isn’t free. Each
useMemo/useCallbackadds comparison cost and memory; eachmemoadds shallow-compare per render. Apply only where measured.” - “The three valid reasons to memoize: expensive computation, referential stability for a
memoboundary, referential stability for auseEffectdep.” - “Profile first with React DevTools Profiler + ‘why did this render.’ Most surprising re-renders trace back to unstable props or context value identity.”
- “
React.memoplus stable props (useMemo/useCallbackupstream) is the combo that actually skips renders. Either half alone usually doesn’t help.” - “Context value should always be memoized — every consumer re-renders on identity change. At scale, split context by domain.”
- “React Compiler (forthcoming) will auto-memoize; the mental model still matters for diagnosing renders and understanding what the compiler does.”
Cross-references
- React profiling (where you measure): 09_react_profiling.md
- Vue equivalent (
v-memo, reactivity model): ../06_vue/13_performance.md - React reconciliation / Fiber: ../05_react/
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