Lesson 14 of 26
Memoization Strategy and Its Costs
React.memo, useMemo and useCallback actually explained as a trade of comparison cost for render cost — and why memoizing everything can make an app slower.
React.memo, useMemo, and useCallback are usually taught as "the
performance hooks" — reach for them and things get faster. That framing
skips the part that actually matters: every one of them is a trade of
comparison cost for render/computation cost, and applied indiscriminately,
that trade can make an app slower, not faster.
What each one actually trades
React.memo wraps a component so it skips re-rendering when its props
are shallow-equal to the previous render. The trade: you pay a shallow
comparison of every prop, on every render, to potentially skip that
component's render work. This is a clear win when the component is genuinely
expensive to render and its props are often stable — a clear loss when the
component is cheap to render (the comparison costs more than just
re-rendering would have) or its props change almost every render anyway
(the comparison runs, finds a difference, and the render happens regardless
— all cost, no benefit).
useMemo caches the result of an expensive computation between
renders, recomputing only when its dependency array changes. The trade: you
pay to store the cached value and to compare the dependency array every
render, to potentially skip recomputing. Worth it for a genuinely expensive
calculation (sorting a large array, a complex derived value); not worth it
for const doubled = value * 2 — checking the dependency array costs more
than just doing the multiplication.
useCallback caches a function reference between renders, so a
child receiving it as a prop sees the same reference (not just equivalent
behavior) across renders where its dependencies haven't changed. Its actual
purpose isn't "functions are slow to create" — they're cheap — it's
specifically to preserve reference equality for a prop that React.memo (or
a dependency array elsewhere) is checking.
The failure mode this fixes — and where it doesn't help alone
// Every render creates a NEW function reference, even though the
// logic is identical — this defeats a memo'd child's shallow comparison.
<MemoizedChild onClick={() => doThing(id)} />// useCallback keeps the same reference across renders, as long as
// `id` hasn't changed — now the memo'd child's comparison can actually pass.
const handleClick = useCallback(() => doThing(id), [id]);
<MemoizedChild onClick={handleClick} />This pairing — useCallback on the parent, React.memo on the child — is
the standard fix for "I memoized the child but it still re-renders on every
parent render." Applying useCallback without a memoized consumer on the
other end buys nothing: nothing is checking that reference for equality, so
preserving it has no observable effect.
Why "memoize everything, it can only help" is wrong
Every memoization has a real, non-zero cost: a comparison on every render (shallow props, or a dependency array), plus memory to hold whatever's cached. For a cheap component or a cheap computation, that comparison cost can genuinely exceed the cost it was meant to avoid — meaning the "optimized" version does strictly more total work than the unoptimized one. Applied broadly across an entire codebase, this doesn't just fail to help, it adds a small tax to every render, everywhere, in exchange for savings that only materialize at specific, genuinely expensive hotspots.
The interview-strength answer to "how would you optimize re-renders here" is never "wrap it in memo" as a reflex — it's identifying, ideally via a profiler, which specific component or computation is both expensive and frequently re-rendered with unchanged inputs, and memoizing precisely that, while leaving cheap, frequently-changing state alone.
What to remember
React.memo,useMemo, anduseCallbackall trade a comparison cost (every render) for a chance to skip a more expensive render or computation — never a free win.useCallbackexists specifically to preserve function reference equality for a memoized consumer downstream; without one, it does nothing useful.- Memoizing something cheap, or something whose inputs change almost every render anyway, can make an app do more total work, not less.
- Target specific, measured hotspots — ideally found with a profiler — rather than applying memoization as a blanket default.
Check yourself
3 questions · pass 3/3 to unlock Core Web Vitals, Images, and Fonts
1.Wrapping a component in
React.memoskips its re-render when props are shallow-equal to last time. What does this comparison itself cost, and why does that matter?2.A parent passes an inline arrow function (
onClick={() => doThing(id)}) as a prop to a child wrapped inReact.memo. Why does this typically defeat the memoization?3.A team memoizes nearly every component and wraps nearly every value in useMemo/useCallback by default, reasoning that 'memoized can't be slower than not memoized.' What's the flaw in that reasoning?
3 left to answer