Advanced⏱️ 11 min📘 Topic 9 of 13

⚡ React Performance — memo, useMemo and useCallback Explained

Stop unnecessary re-renders in React. Learn React.memo, useMemo and useCallback — when they help, when they don't, and how to measure with React DevTools profiler.

The biggest React performance trap: re-rendering more than needed. Three tools help:

  • React.memo — memoize a component (skip render if props unchanged)
  • useMemo — memoize a computed value
  • useCallback — memoize a function reference

⚠️ Reality check first

React is fast. Profile before optimizing. Most apps don't need any of these. Premature memoization clutters code and sometimes makes things slower.

🟢 React.memo

const Row = React.memo(function Row({ user }) {
  return <li>{user.name}</li>;
});

Row skips re-render if user reference is the same. Use shallow equality by default; pass a custom comparator for objects.

🟣 useMemo — cache expensive computations

const filtered = useMemo(
  () => users.filter(u => u.active),
  [users]
);

🔵 useCallback — stable function reference

const onClick = useCallback(() => doStuff(id), [id]);

Needed when passing handlers to memoized children — otherwise a new function ref each render defeats React.memo.

🧪 The profiler

React DevTools → Profiler tab → record an interaction → see which components rendered and why. Always profile before optimizing.

💻 Code Examples

memo + useCallback combo

const Row = React.memo(function Row({ user, onSelect }) {
  console.log('Render', user.id);
  return <li onClick={() => onSelect(user.id)}>{user.name}</li>;
});

function List({ users }) {
  const [selected, setSelected] = useState(null);
  // ✅ stable — Row.memo works
  const onSelect = useCallback(id => setSelected(id), []);
  return <ul>{users.map(u => <Row key={u.id} user={u} onSelect={onSelect} />)}</ul>;
}
Output:
Rows only re-render when their user changes, not when selection changes.

useMemo for derived value

const sorted = useMemo(
  () => [...items].sort((a, b) => a.price - b.price),
  [items]
);
Output:
Sort runs only when `items` reference changes.

⚠️ Common Mistakes

  • Wrapping every component in React.memo — usually no benefit, sometimes slower (comparison cost > render cost).
  • Using useMemo for trivially cheap calculations (e.g. addition) — pure overhead.
  • Forgetting that React.memo only does SHALLOW prop comparison — new object literals defeat it.
  • Skipping useCallback for a handler that's never passed to a memoized child — useless ceremony.

🎯 Interview Questions

Real questions asked at top product and service-based companies.

Q1.What does React.memo do?Intermediate
It memoizes a component so it skips re-rendering when its props are shallow-equal to the previous render. Useful for pure components rendered inside frequently-updating parents.
Q2.What's the difference between useMemo and useCallback?Intermediate
useMemo memoizes the result of a function. useCallback memoizes the function itself. `useCallback(fn, deps)` is equivalent to `useMemo(() => fn, deps)`.
Q3.When does useCallback NOT help?Advanced
When the function isn't passed to a memoized child or used in a dependency array — you're just paying for memoization without using it. Profile to confirm.
Q4.Why might memoizing cause a bug?Advanced
Memoized references skip re-runs based on dependency equality. If a dep is missing (forgotten in the array), you'll see stale closures with wrong values. Use `eslint-plugin-react-hooks` exhaustive-deps to catch these.
Q5.How would you find out which component is causing re-renders?Advanced
Open React DevTools → Profiler → click 'Record' → interact → 'Stop'. The flame graph shows render times; the 'Why did this render?' tooltip explains props/state/context changes.

🧠 Quick Summary

  • Profile FIRST — most apps don't need memoization.
  • React.memo skips re-render if props are shallow-equal.
  • useMemo caches values; useCallback caches functions.
  • Memoization works only when references stay stable.
  • Exhaustive-deps lint rule prevents stale closures.