AniUI Academy

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.

11 min read

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, and useCallback all trade a comparison cost (every render) for a chance to skip a more expensive render or computation — never a free win.
  • useCallback exists 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

up to 50
  1. 1.Wrapping a component in React.memo skips its re-render when props are shallow-equal to last time. What does this comparison itself cost, and why does that matter?

  2. 2.A parent passes an inline arrow function (onClick={() => doThing(id)}) as a prop to a child wrapped in React.memo. Why does this typically defeat the memoization?

  3. 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