Skip to content

When to Optimize

useMemo, useCallback, and React.memo are optimization tools. Like any tool, using them incorrectly causes more problems than they solve. This lesson gives you a framework for deciding when optimization is worth it.

Every optimization hook has real costs:

  • Memory: Cached values are held in memory even when you do not need them.
  • Comparison overhead: Every render compares dependencies (for useMemo/useCallback) or props (for React.memo).
  • Complexity: More hooks make code harder to read and debug.
  • False confidence: Incorrect dependencies can cause stale data bugs that are hard to trace.

These costs are worth paying when the optimization is measurably faster. They are not worth paying by default.

React renders are measured in milliseconds. The browser paints at 60fps — you have 16.6ms per frame. A render that takes 0.1ms re-running a reduce on a 10-item array saves essentially nothing by memoizing. The comparison overhead of useMemo might take longer than the computation it avoids.

Optimization becomes relevant when:

  • A render takes more than ~2ms (measure in React DevTools Profiler)
  • A large list (hundreds or thousands of items) re-renders every time
  • An expensive computation (complex data transformation, sorting a large dataset) runs on every render
  1. Write the code without optimization. Make it correct first.
  2. Measure if it feels slow. Use the React DevTools Profiler to see which components render and how long they take.
  3. Optimize the bottleneck. Apply useMemo, useCallback, and React.memo to the specific component or calculation that is slow — not everywhere.
  4. Verify the optimization worked. Profile again. If the numbers are the same, remove the optimization.

Wrapping every handler in useCallback:

const handleClick = useCallback(() => setCount(c => c + 1), []);

If handleClick is not passed to a memoized component and not used as a useEffect dependency, this does nothing useful.

Wrapping every calculation in useMemo:

const doubled = useMemo(() => count * 2, [count]);

count * 2 is a single multiplication. useMemo costs more than the computation.

Adding React.memo to every component: If props change on every render anyway (because they are new objects), React.memo just adds comparison overhead without preventing any renders.

ZeroBudget uses optimization judiciously:

  • spentByCategory uses useMemo — it iterates all transactions and the result is passed to every CategoryCard.
  • totalIncome and totalBudgeted use useMemo — they feed the leftToAssign calculation.
  • Action callbacks use useCallback — they are passed to CategoryCard, which uses React.memo.
  • CategoryCard uses React.memo — 8 cards re-rendering on every state change is measurable overhead.

Everything else renders without memoization. The income section, the transaction form, and the month nav are fine without it — they are small, fast, and do not re-render frequently.

  1. Open React DevTools in your browser. Enable the Profiler and record a session where you add an income source.
  2. Look at which components re-rendered and how long each took.
  3. Identify one component that re-rendered unnecessarily (it received the same props as before).
  4. Apply the appropriate optimization (React.memo, useCallback, or useMemo) and profile again to confirm the improvement.
  • useMemo, useCallback, and React.memo have real costs — use them where you can measure the benefit.
  • Write correct code first. Profile. Optimize bottlenecks. Profile again.
  • Do not optimize small calculations, rarely-rendered components, or handlers that are not passed to memoized children.
  • The memoization triad works together: React.memo on the component, useCallback for functions, useMemo for objects.