React.memo & Performance
React.memo skips re-render on shallow-equal props; inline objects/functions/children break it—stabilize with hooks.
React.memo wraps a component so it skips re-rendering when its props are shallow-equal to the previous render. It addresses one specific cause of wasted renders: a parent re-rendering that pushes unchanged props to a child.
What actually causes a re-render 🎯#
A component re-renders when:
- Its own state changes (
setState/dispatch). - Its parent re-renders (default: children re-render too).
- A context it consumes changes value.
Note what's not on the list: props changing is a consequence of a parent re-rendering, not an independent trigger. By default React re-renders children whenever the parent renders—React.memo is how you opt a child out of #2 when its props didn't actually change.
const Row = React.memo(function Row({ item, onSelect }) {
return <li onClick={() => onSelect(item.id)}>{item.name}</li>;
});
// Custom comparator (rarely needed): return true to SKIP render
const Row2 = React.memo(RowImpl, (prev, next) => prev.item.id === next.item.id);
The memo decision#
flowchart TD
A["Parent re-renders"] --> B{"Child wrapped in React.memo?"}
B -->|No| RR["Child re-renders"]
B -->|Yes| C{"Props shallow-equal?"}
C -->|Yes| SKIP["Skip re-render 🟢"]
C -->|No| RR
What breaks memo ⚠️#
Shallow equality compares each prop with Object.is. Any prop that's a new reference each render defeats memo:
function Parent() {
return (
<MemoChild
style={{ color: "red" }} // ⚠️ new object every render
onClick={() => doThing()} // ⚠️ new function every render
>
<Icon /> // ⚠️ children is a new element object too
</MemoChild>
);
}
🟢 Fix by stabilizing the references:
const style = useMemo(() => ({ color: "red" }), []);
const onClick = useCallback(() => doThing(), []);
// For children: hoist static JSX out, or pass it from a parent that doesn't re-render
⚠️ The children prop is an especially common leak—passing JSX children makes a new element each render, so a memoized wrapper still re-renders. Composition (passing children from a higher, stable parent) can sidestep this.
Keys 🎯#
Lists need stable, unique keys so React can match elements across renders. Using array index as key causes bugs when the list reorders or items are inserted/removed—React reuses the wrong DOM/state.
{items.map(i => <Row key={i.id} item={i} />)} // 🟢 stable id
{items.map((i, idx) => <Row key={idx} />)} // ⚠️ index → state bleed on reorder
Changing a component's key also forces a remount (fresh state)—a deliberate technique to reset a subtree.
List virtualization#
For long lists (hundreds/thousands of rows), rendering them all is the real bottleneck—not re-renders. Virtualize: render only the visible window plus a small buffer.
import { FixedSizeList } from "react-window";
<FixedSizeList height={400} itemCount={items.length} itemSize={35} width="100%">
{({ index, style }) => <div style={style}>{items[index].name}</div>}
</FixedSizeList>
Libraries: react-window, react-virtualized, @tanstack/react-virtual.
The Profiler#
Measure before optimizing. Use the React DevTools Profiler to record renders and see which components rendered, why, and how long they took ("flamegraph" + "ranked" views; enable "Record why each component rendered"). The <Profiler onRender={cb}> API captures the same data programmatically.
Deferring work (brief) 🎯#
Concurrent features let you keep the UI responsive without memo tricks—see Concurrent Features for detail:
useTransitionmarks a state update as non-urgent, so typing/clicks stay responsive while an expensive list re-renders.useDeferredValuerenders with a "lagging" copy of a fast-changing value.
const [isPending, startTransition] = useTransition();
startTransition(() => setQuery(input)); // heavy filtering won't block the input
Interview Q&A#
Q1. What exactly does React.memo do?
It memoizes a component so that when its parent re-renders, the component skips re-rendering if its props are shallow-equal to the previous props. It does nothing for state or context changes.
Q2. What are the actual triggers for a component to re-render? Its own state/reducer update, its parent re-rendering, or a consumed context's value changing. Props "changing" is just a symptom of the parent re-rendering.
Q3. Why does a memoized child still re-render even though "nothing changed"?
A prop is a new reference each render—an inline object, inline function, or JSX children. Shallow equality sees a different reference and bails out of the skip. Stabilize with useMemo/useCallback or composition.
Q4. Why is using array index as a key problematic? On reorder, insertion, or deletion the index no longer maps to the same logical item, so React reuses elements incorrectly—stale DOM state, wrong inputs, animation glitches. Use a stable unique id.
Q5. When is virtualization the right tool instead of memoization? When the cost is rendering a very large number of DOM nodes at once. Virtualization renders only the visible window, cutting DOM size regardless of re-render frequency—memo alone won't help there.