When memoization helps vs hurts
Memoization (memo/useMemo/useCallback) helps when it prevents an expensive computation or a costly subtree re-render, AND the deps are actually stable. It hurts when the work is cheap, deps change every render, or you wrap everything reflexively — then you pay comparison + memory cost for nothing.
Memoization is a trade: you spend memory + a dependency comparison every render to maybe skip work. Whether it's worth it depends on the work and the dep stability.
When it HELPS
1. Genuinely expensive computation — useMemo for sorting/filtering a large list, heavy derived data, expensive parsing. Skipping it on unrelated renders is a real win.
2. Expensive subtree re-renders — React.memo on a component whose render is costly (big tree, heavy children) and whose props usually don't change.
3. Stabilizing references for memo'd consumers — useCallback/useMemo to keep a prop reference stable so a React.memo child or a useEffect dep array doesn't fire needlessly.
4. Referential identity correctness — sometimes you need a stable object/array reference for an effect or a context value.
The common thread: the saved work must be bigger than the memoization overhead, and the dependencies must actually be stable often enough to score cache hits.
When it HURTS
1. The work is cheap. Memoizing a + b or rendering a <span> costs more (storage + comparison) than just doing it.
2. Dependencies change every render. useMemo(fn, [{}]) or deps that are new objects/arrays each render → 0% hit rate → pure overhead.
3. React.memo with always-changing props. If a parent passes a new inline object/function each render, memo runs a comparison that always fails — extra work, no skip.
4. Reflexive over-application. Wrapping every value and function "to be safe" bloats code, adds comparison cost everywhere, and makes deps arrays a maintenance burden.
5. It hides the real problem. Often the fix isn't memoization — it's moving state down, splitting components, or using children so the expensive subtree isn't re-rendered at all.
How to decide
Measure first. Use the React Profiler — is this component actually re-rendering hot, and is its render actually expensive? Memoize the proven hotspot; leave the rest alone. Default to not memoizing; add it when the profiler points at something.
(React Compiler changes this calculus — it auto-memoizes — but the mental model still matters.)
The framing
"It's a cost/benefit trade: every memo costs a comparison and some memory each render, and only pays off if it skips work that's bigger than that cost AND the deps are stable enough to hit. It helps for expensive computations, costly stable subtrees, and reference stabilization for memo'd consumers. It hurts when the work is cheap, deps churn every render, or you apply it reflexively. So: profile first, memoize the proven hotspot, and prefer structural fixes — lifting state down, children — over scattering useMemo everywhere."
Follow-up questions
- •How do you measure whether a component is a re-render hotspot?
- •Why does React.memo with inline object props not help?
- •How does passing children avoid re-rendering a subtree without memo?
- •How does the React Compiler change when you'd hand-memoize?
Common mistakes
- •Wrapping everything in useMemo/useCallback 'to be safe'.
- •Memoizing with dependencies that change every render — zero hit rate.
- •React.memo on a component that always gets new prop references.
- •Memoizing cheap work where the comparison costs more than the work.
- •Reaching for memoization instead of fixing the structural cause of re-renders.
Performance considerations
- •Every memo stores a value and runs a dependency comparison on each render. Net benefit = (work skipped × hit rate) − (comparison + memory cost). Profile to confirm the component is hot and the render is expensive before adding it.
Edge cases
- •A dep that's a new object/array literal every render.
- •useMemo used for referential identity correctness, not speed — still valid.
- •Memoized component whose children prop is a new element each render.
Real-world examples
- •useMemo around sorting/filtering a 10k-row table — clear win.
- •A codebase where useCallback is on every handler but most have no memo'd consumer — pure overhead.