Topics in this subject
React 4 min read Updated 5 Aug 2026

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:

  1. Its own state changes (setState/dispatch).
  2. Its parent re-renders (default: children re-render too).
  3. 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:

  • useTransition marks a state update as non-urgent, so typing/clicks stay responsive while an expensive list re-renders.
  • useDeferredValue renders 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.