Memoization Performance Tradeoffs
React.memo, useMemo, useCallback, and cache maps: when memoization helps, when it wastes memory and time.
- performance
- memoization
- react
- usememo
- profiling
Memoization stores previous results of pure computations or render outputs so you skip repeat work. Used blindly (useMemo on everything), it adds memory, comparison cost, and noise. Used on proven hotspots, it cuts wasted renders and CPU.
Docs: React useMemo, web.dev render performance, Profiler mindset.
Forms you’ll touch
| Tool | Caches |
|---|---|
useMemo(() => compute(x), [x]) |
Computed value |
useCallback(fn, deps) |
Function identity |
React.memo(Component) |
Last render output |
Map / LRU cache |
Explicit data |
| Selector memo (Redux/reselect) | Derived state |
When it helps
const sorted = useMemo(
() => hugeList.toSorted((a, b) => a.name.localeCompare(b.name)),
[hugeList],
);
- Expensive pure transforms called often with same inputs
- Child
memocomponents that re-render from unstable function/object props - Derived data shared across many children
When it hurts or does nothing
const value = useMemo(() => a + b, [a, b]); // pointless for cheap math
- Dependency changes every render (new object literals upstream)
- Comparison cost ≥ compute cost
- Memoizing huge arrays that still invalidate often → memory pressure
- Hiding unnecessary parent state design problems
Identity traps
// New options object every render → child memo useless
<Chart options={{ color: 'red' }} />
// Stable
const options = useMemo(() => ({ color: 'red' }), []);
<Chart options={options} />
useCallback only matters if identity is consumed (deps of effects, memo children).
Measure first
- React Profiler: which components commit often?
- Why did they render (prop/state/context)?
- Fix state colocation / split context before memo spray.
- Add memo to the expensive leaf.
Non-React caches
const cache = new Map();
export function parse(schema, input) {
const key = hash(schema, input);
if (cache.has(key)) return cache.get(key);
const value = expensive(schema, input);
cache.set(key, value);
return value;
}
Bound cache size (LRU). Unbounded maps are memory leaks — memory profiling.
Interview out-loud
“Memoization skips pure recompute when inputs are stable. I profile first, fix state architecture, then use useMemo/memo on expensive hotspots. Memo with unstable deps is dead weight and can increase memory.”
How this shows up in interviews
Be ready to define the metric or technique in one sentence, name one measurement approach (DevTools panel, web-vitals, or headers), and cite a concrete fix you would try first. Walk through a before/after: what the waterfall or flame chart showed, what you changed, and which percentile moved. Mention a tradeoff (complexity, caching correctness, or third-party business constraints) so the answer doesn’t sound like a blog checklist.
Production guardrails
Ship behind a flag when the change is risky, watch field p75 for the affected template for at least a few days, and keep a rollback path. Pair lab verification (throttled Performance/Network) with RUM so you don’t celebrate a Lighthouse-only win. Document the owner of any ongoing budget or third-party exception.
Related
- Virtual lists performance
- Long tasks and yielding
- Browser DevTools Performance panel
- JavaScript bundle budget
Further depth
Teams often under-invest in this topic until an incident or CWV regression. Schedule a one-hour drill: reproduce the failure mode in DevTools, list the top three mitigations for your stack, and file tickets with owners. Revisit after the next major feature that touches networking, rendering, auth, or third parties — those are the moments regressions land. Keep primary documentation links in the runbook so on-call is not searching chat history at 2am.
Concrete artifacts to leave behind: a short architecture note, a CI assertion or header snapshot, and a dashboard panel (lab or field) that would have caught the last bug. Teaching the rest of the team the mental model matters as much as the one-line fix.
Further reading
Related guides
- Virtual Lists PerformanceRender only visible rows: windowing mental model, overscan, scroll issues, and accessibility tradeoffs.
- Avoiding Layout ThrashingStop forced sync layout loops: batch DOM reads and writes, use rAF, and fix janky measurement code.
- Caching Static Assets FingerprintingContent-hash filenames, long-cache headers, HTML revalidation, and CDN invalidation without stuck users.
- CLS Optimization TacticsFix cumulative layout shift: dimensions, font metrics, reserved slots, and stable late-loading UI.
- Code Splitting StrategiesRoute, component, and vendor splits: dynamic import(), magic comments, and avoiding over-splitting waterfalls.