Skip to content
RungsySign in

React Performance

Measure, then memo — in that order, or you'll add complexity for nothing.

45 minDifficulty 4/5react · performanceAI-writtenWritten by a model on 16 August 2026 and not yet read by a person. Checked automatically: schema, the pedagogical rules the hand-written material is held to, and every diagram parsed for real.

Before this

Why this exists

A developer wraps every component in `React.memo` and every value in `useMemo`, and the app gets slightly SLOWER — because memoization itself has a cost (comparing props, storing cached values), and applying it where nothing was actually expensive just adds overhead with no benefit. Measure first, or you're optimizing blind.

The mental model

Performance work in React has one correct order: measure where time is actually going (usually with the React DevTools Profiler), THEN apply the specific tool that addresses that specific bottleneck. Memoization tools don't make anything universally faster — they trade CPU time doing comparisons for CPU time skipping work, and that trade is only a win when the skipped work was genuinely expensive.

How it works

React.memo skips re-rendering when props are shallowly equal

Wrapping a component in `React.memo` makes React compare its new props to its previous props (shallowly, by reference for objects/arrays/functions) before calling the function again — if they're equal, the previous render's output is reused and the function doesn't run. This only helps when the component actually re-renders often with UNCHANGED props, and it costs a comparison on every render regardless.

A new function or object prop defeats memo silently

`<Child onClick={() => doThing()} />` creates a brand-new function on every parent render — even wrapped in `React.memo`, Child sees a 'different' `onClick` by reference every time and re-renders anyway. `useCallback` gives that function a stable identity across renders where its own dependencies haven't changed, which is what actually makes the memo comparison succeed.

useMemo caches a computed value, not a re-render

`useMemo(() => expensiveSort(items), [items])` re-runs `expensiveSort` only when `items` changes, reusing the cached result otherwise — appropriate when the computation is genuinely costly. Wrapping a cheap calculation (like adding two numbers) in `useMemo` adds the overhead of dependency comparison for no benefit, since the calculation itself was already faster than the comparison.

The Profiler shows you where time is actually spent, before you guess

The React DevTools Profiler records an interaction and shows exactly which components rendered, how long each took, and why (which prop or state change triggered it). This turns performance work from guessing into measurement — optimizing a component that takes 0.1ms is wasted effort compared to one taking 40ms, and the Profiler is how you actually know which is which.

The mechanism

Before applying any optimization, profile the actual interaction to find which component's render is genuinely slow and why it's re-rendering unnecessarily. If the cause is a component re-rendering with unchanged props, wrap it in React.memo — and ensure the props passed to it (functions, objects) are themselves stable via useCallback or useMemo, since an unstable prop defeats the memo comparison entirely.

What people get wrong

Wrapping everything in React.memo and useMemo is a safe default that can only help.
Every memoization has a real cost — storing the cached value, comparing dependencies or props each render — and applying it to cheap components or computations makes things marginally SLOWER, not faster, with added code complexity as a bonus cost. Blanket memoization is a common overreaction to vague performance concerns, and it makes the codebase harder to read without the profiling data to justify that it actually helped anything.
React.memo alone guarantees a component won't re-render unnecessarily.
React.memo only helps if the props passed to that component are ALSO stable across renders — an inline function or object literal as a prop creates a new reference every time, which defeats the shallow comparison memo relies on. This is why memo 'doesn't seem to work' so often in practice: the memoized component is correctly configured, but its parent is still passing it a fresh function reference on every render.
Re-renders are inherently slow and should always be minimized.
The render phase (calling functions) is usually cheap; the expensive part is the DOM commit for genuine changes — a re-render that produces no DOM change costs very little, so minimizing re-renders for their own sake, without profiling data, often isn't worth the added complexity. Conflating 're-render' with 'slow' leads to premature optimization targeting the wrong thing, per `react-rendering-model`'s distinction between the render and commit phases.

When not to use it

A component is cheap to render and doesn't re-render often.
Nothing — leave it unmemoized. The cost of the memo comparison itself likely exceeds any savings for a genuinely cheap component.
You haven't profiled the app and don't yet know where time is actually going.
Open the React DevTools Profiler and measure a real interaction first — applying any optimization before measuring is a guess, not engineering.

Terms

React.memo
A wrapper that skips re-rendering a component when its props are shallowly equal to the previous render's props.
useCallback
A hook that returns a stable function reference across renders, as long as its own listed dependencies haven't changed — commonly used to keep a callback prop stable for a memoized child.
useMemo
A hook that caches the result of a computation, re-computing it only when its listed dependencies change.
React DevTools Profiler
A browser extension panel that records component render times and causes during an interaction, used to find genuine performance bottlenecks before optimizing.

In an interview

You've wrapped a child component in React.memo, but it still re-renders every time its parent does. What's the most likely cause?

  • a prop passed to the child (commonly a function or object) is a new reference on every parent render
  • React.memo's shallow comparison sees this new reference as a change even if the underlying data is the same
  • wrapping the prop in useCallback or useMemo, with correct dependencies, fixes this by keeping its reference stable

Can you recall it?

Why can wrapping every component in React.memo make an app slower rather than faster, and what should you do before applying it?

Keep track of this

Add React in Practice to your map and Rungsy will schedule reviews so you actually remember it.