React.memo & Performance
React.memo skips re-render on shallow-equal props; inline objects/functions/children break it—stabilize with hooks.
🧑🏫 Sabse pehle — simple mein samjho#
Jab bhi kisi component ki state ya props badalti hai, wo dobara render (re-render) hota hai — aur uske saare children bhi. Kabhi-kabhi child ki props same hoti hain phir bhi wo bekaar mein dobara render ho jaata hai, jisse time/paisa waste hota hai. React.memo component ko bolta hai "agar tumhari props same hain to dobara render mat karo, chill karo". Jaise agar order same hai to waiter ko dobara kitchen jaane ki zaroorat nahi.
// props same rahe to Child dobara render nahi hoga
const Child = React.memo(function Child({ name }) {
console.log("rendered:", name);
return <p>{name}</p>;
});
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<Child name="Tarun" /> {/* count badle bhi ye re-render nahi hoga */}
</>
);
}
Yaad rakho: React.memo = props same hain to re-render skip — time bachao.
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.
Real-world example: a WhatsApp-style chat list 💬#
Picture a chat screen: a list of MessageBubble components above a message input. Typing in the input updates local state in the parent ChatScreen, which re-renders — and by default, every bubble in the list re-renders too, even though none of their own data changed.
// ⚠️ No memo — every keystroke in the input re-renders ALL message bubbles
function MessageBubble({ message }) {
console.log("rendering bubble", message.id); // fires for every bubble, every keystroke
return <div className="bubble">{message.text}</div>;
}
function ChatScreen({ messages }) {
const [draft, setDraft] = useState("");
return (
<>
{messages.map((m) => (
<MessageBubble key={m.id} message={m} />
))}
<input value={draft} onChange={(e) => setDraft(e.target.value)} />
</>
);
}
// 🟢 React.memo — a bubble only re-renders if ITS OWN `message` prop changes
const MessageBubble = React.memo(function MessageBubble({ message }) {
console.log("rendering bubble", message.id); // only fires when this message changes
return <div className="bubble">{message.text}</div>;
});
function ChatScreen({ messages }) {
const [draft, setDraft] = useState("");
return (
<>
{messages.map((m) => (
<MessageBubble key={m.id} message={m} />
))}
<input value={draft} onChange={(e) => setDraft(e.target.value)} />
</>
);
}
With hundreds of messages in the chat, this is the difference between every keystroke re-rendering the whole list versus re-rendering just the input.
flowchart TD
T["User types in message input"] --> P["ChatScreen re-renders"]
P --> Q{"MessageBubble wrapped in React.memo?"}
Q -->|"No"| C1["Bubble 1 re-renders ⚠️"]
Q -->|"No"| C2["Bubble 2 re-renders ⚠️"]
Q -->|"No"| C3["Bubble N re-renders ⚠️"]
Q -->|"Yes, props unchanged"| S1["Bubble 1 skips 🟢"]
Q -->|"Yes, props unchanged"| S2["Bubble 2 skips 🟢"]
Q -->|"Yes, props unchanged"| S3["Bubble N skips 🟢"]
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.