Skip to content

Memoization & the Compiler

Every render re-runs your component, which re-creates every value inside it — including objects, arrays, and functions. Those are new references each time, even if their contents are identical. Usually that’s fine. It becomes a problem in two cases:

  1. An expensive calculation runs on every render when its inputs didn’t change.
  2. A new reference (a function or object) passed to a memoized child breaks that child’s memoization, causing needless re-renders.

The three memoization tools each address a piece of this:

ToolCachesUse when
useMemo(fn, deps)a value (the result of fn)a calculation is expensive, or you need a stable object/array reference
useCallback(fn, deps)a function (a stable reference)you pass a callback to a memoized child (it’s useMemo for functions)
React.memo(Component)a component’s rendera component re-renders with the same props and rendering it is costly
// Expensive calc, cached until inputs change:
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
// Stable function reference so a memoized child does not re-render:
const handleSelect = useCallback((id) => setSelected(id), []);
// Skip re-render when props are shallowly equal:
const Row = React.memo(function Row({ item }) { /* ... */ });

When memoization helps — and when it hurts

Section titled “When memoization helps — and when it hurts”

Memoization is not free: it costs memory and a comparison on every render. Applied everywhere “just in case,” it makes code noisier and can be a net negative. It genuinely helps when:

  • the cached calculation is measurably expensive, or
  • you need a stable reference to keep a React.memo child (or an effect dependency) from re-running.

For cheap values — a string concat, a small .filter — computing during render is fine and clearer. Don’t reach for useMemo reflexively.

flowchart TB
  render["parent re-renders"] --> newfn["new function reference
created each render"]
  newfn --> child["memoized child sees
a 'changed' prop"]
  child --> rerender["child re-renders needlessly"]
  fix["useCallback → stable reference"] --> skip["memoized child skips re-render"]
Referential equality is why callbacks break memoized children

React ships a compiler that automatically memoizes your components and values at build time — it inserts the equivalent of useMemo/useCallback/memo for you, precisely where they help, by analyzing your code. Its whole point is that you should not need to memoize by hand.

The practical guidance for React 19+ projects:

  • With the compiler enabled: write plain, straightforward code. Don’t scatter useMemo/useCallback — the compiler handles memoization for you, more correctly and more granularly than most hand-tuning. Your job is to keep components pure (the compiler relies on the Rules of Hooks and purity to be safe).
  • Without the compiler: the manual tools above still apply — reach for them only at real, measured bottlenecks.

The direction of travel is clear: manual memoization is becoming an optimization of last resort rather than a default habit. Keep your components pure and let the compiler do the bookkeeping.

What does useCallback do?
Why can passing an inline function to a React.memo child defeat the memoization?
What is the guidance when the React Compiler is enabled?
When is manual memoization genuinely worth it (without the compiler)?