Skip to main content

Performance

Memoisation in React

Written by Published

Re-rendering is not slow. React re-runs a function and compares the result - that is cheap. Memoise when you have measured a problem, not because a component renders often.

The three tools

  • React.memo - skip re-rendering a component when its props are unchanged.
  • useMemo - reuse a computed value between renders.
  • useCallback - reuse a function's identity between renders.

All three trade memory and complexity for skipped work. All three are pointless if the work they skip was cheap to begin with.

Why memo alone often does nothing

React.memo compares props by identity. If the parent passes an inline arrow or object literal, that prop is new on every render and the memo never matches.

memo defeated, then working

jsx

const Child = React.memo(function Child({ label, onAction }) {
  const renders = React.useRef(0)
  renders.current++
  return <p>{label} rendered {renders.current} times</p>
})

function App() {
  const [tick, setTick] = React.useState(0)

  // New function every render — memo cannot match it.
  const unstable = () => {}

  // Stable identity across renders.
  const stable = React.useCallback(() => {}, [])

  return (
    <div>
      <button onClick={() => setTick(tick + 1)}>Re-render parent ({tick})</button>
      <Child label="With inline arrow" onAction={unstable} />
      <Child label="With useCallback" onAction={stable} />
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<App />)

Press the button a few times. The first child re-renders every press despite being wrapped in memo; the second does not. Memoising a component without stabilising its props achieves nothing.

useMemo for expensive work

useMemo keeps a computed value until its dependencies change. It is worth it when the computation is genuinely expensive - sorting thousands of rows - and worth nothing when it is arithmetic.

Skipping real work

jsx

function slowTotal(n) {
  let total = 0
  for (let i = 0; i < n * 200000; i++) total += i % 3
  return total
}

function Demo() {
  const [size, setSize] = React.useState(1)
  const [tick, setTick] = React.useState(0)

  // Recomputed only when size changes, not when tick does.
  const total = React.useMemo(() => slowTotal(size), [size])

  return (
    <div>
      <p>Total: {total}</p>
      <button onClick={() => setSize(size + 1)}>Change size ({size})</button>{" "}
      <button onClick={() => setTick(tick + 1)}>Unrelated re-render ({tick})</button>
      <p style={{ color: "#64748b" }}>
        The second button re-renders without redoing the work.
      </p>
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Demo />)

Measure before optimising

Every memoisation has a cost: React stores the previous value and compares dependencies on each render. For cheap work that cost exceeds the saving, and you have added a dependency array that can now be wrong.

  1. Notice something actually feels slow.
  2. Profile it - React DevTools shows which components render and how long they take.
  3. Fix the real cause, which is often an unnecessary state update rather than a slow render.
  4. Memoise only if it is still slow.

The two cases where memoising up front is reasonable: a context value, because every consumer depends on its identity, and a dependency array where an object would otherwise re-trigger an effect on every render.