Back to Performance
Performance
medium
senior

How do you use memoization to reduce unnecessary 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.

7 min read·~15 min to think through

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.

tsx
// 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:

tsx
// 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:

tsx
// 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:

  1. Adopt the compiler. Removes 80% of manual memoization.
  2. Don't preemptively memoize. Measure first. The compiler will get it.
  3. 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 functionsmemo(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 AppContext causes 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.

Related questions