Memoization to reduce re-renders
Memoization stops re-renders by giving React's diff stable references and stable child props. `React.memo` skips child renders when props are shallow-equal; `useMemo` caches expensive values; `useCallback` caches function identity. In React 19+, the React Compiler does most of this automatically — manual memoization is a fallback, not the default.
React re-renders a component when its parent re-renders or its state/context changes. Most of that work is cheap. The optimization only matters when a re-render is expensive (big subtree, heavy computation) or wide (re-rendering everything when only one row changed).
Three tools.
// 1. React.memo — skip re-render if props are shallow-equal
const Row = React.memo(function Row({ item }) { ... });
// 2. useMemo — cache the result of an expensive computation
const sorted = useMemo(() => heavySort(items), [items]);
// 3. useCallback — cache a function reference
const onClick = useCallback((id) => select(id), [select]);Why useCallback matters. A new function each render is a new prop reference. React.memo(Child) does Object.is on each prop — a fresh function breaks the bailout:
// Bad — Row.memo is useless, new onClick every render
function List() {
return items.map(i => <Row key={i.id} item={i} onClick={() => select(i.id)} />);
}
// Good — stable identity
function List() {
const onClick = useCallback((id) => select(id), [select]);
return items.map(i => <Row key={i.id} item={i} onClick={onClick} />);
}Why useMemo is mostly a referential-equality tool, not a perf tool.
The common case for useMemo isn't that the computation is slow — it's that the result is passed to a memoized child. A new object each render breaks memoization downstream:
// Each render makes a new style object → breaks any memoized child receiving it
<Header style={{ color }} />
// Stable identity
const style = useMemo(() => ({ color }), [color]);
<Header style={style} />The big shift: React Compiler. In React 19+, the React Compiler automatically inserts the equivalent of useMemo/useCallback everywhere it's safe. The recommended posture in 2026:
- Adopt the compiler. Removes 80% of manual memoization.
- Don't preemptively memoize. Measure first. The compiler will get it.
- Use Profiler to find what's actually re-rendering.
The pre-compiler reflex of "wrap every callback in useCallback" is now a code-smell unless you've measured.
When manual memoization is still right.
- Genuinely heavy computation (sorting 10k items, parsing markdown, formatting a chart series). The compiler memoizes for referential equality, not because it knows the cost.
- Stable props for memoized children below a compiler boundary you don't control (legacy code, third-party).
- Custom equality functions —
memo(Comp, (prev, next) => ...)when shallow equality isn't right (e.g., comparing by id).
When memoization is wrong / wasted.
- Component is simple (returns a few elements) — the comparison cost is similar to re-rendering.
- Props include a fresh object/array every render anyway — memoization always misses.
- Children that re-render often regardless (live data, animations).
- Inline objects/arrays in JSX — wrap source in memo isn't enough if the parent rebuilds the input each render.
The render-counter trick. When debugging "why is this re-rendering?", drop useEffect(() => console.log("render", props)); or use why-did-you-render. The Profiler tab in React DevTools shows render counts and what changed.
Patterns that obviate memoization.
- Lift state down. A counter that lives in
<App>re-renders everything; move it into the leaf that uses it. - Split context. A single
AppContextcauses everything subscribed to re-render on any change. Split into per-domain contexts (auth, theme, cart) so consumers only see their slice. - Children-as-props. Pass children through a memoized wrapper; React sees the same children element across renders.
Senior framing. Memoization is a targeted fix for measured re-render problems. The default in 2026 is: write idiomatic code, run the compiler, profile, then add manual memoization at the bottleneck. The old-school "memoize everything to be safe" practice now hurts more than it helps (compiler thrash, harder reads, masked structural issues).
Follow-up questions
- •How does the React Compiler decide what to memoize?
- •When is `useMemo` cheaper than just recomputing?
- •Why is custom equality in `React.memo` risky?
- •How do you split context to avoid unnecessary subscriptions?
Common mistakes
- •Wrapping every callback in useCallback even when nothing memoized consumes it.
- •Memoizing a child while still passing it a fresh object/array prop.
- •Trusting memoization to fix a structural problem (state in the wrong place).
- •Using `React.memo` on components with `children` — the children element identity churns anyway.
Performance considerations
- •Memo comparison has a cost — for tiny components it can be slower than just re-rendering.
- •Profile before/after; React Profiler's "why did this render" shows the offending prop.
- •useMemo's dep array runs every render — heavy deps = useMemo can be a net loss.
Edge cases
- •Custom equality + future code change → equality stays the same → stale UI.
- •Memoized component reading from context still re-renders on context change.
- •Strict Mode runs effects twice — memoized fns are still stable across the double-invocation.
Real-world examples
- •Big tables (selection grids, spreadsheets) — memoize rows, stabilize callbacks.
- •Charting libraries — memoize series-derivation, stabilize the data prop.