Performance, memoisation and transitions

Measure first with the Profiler, when memo, useMemo and useCallback pay for themselves, and keeping input responsive with transitions and deferred values.

Measure before you optimise

import { Profiler } from 'react'

// the Profiler reports every commit, so an expensive tree becomes visible
function onRender(id, phase, actualDuration, baseDuration) {
  if (actualDuration > 16) {                 // longer than one frame
    console.log(id, phase, Math.round(actualDuration) + 'ms', Math.round(baseDuration) + 'ms')
  }
}

export function App({ guides }) {
  return (
    <Profiler id="GuideList" onRender={onRender}>
      <GuideList guides={guides} />
    </Profiler>
  )
}
  • In React DevTools, enable Highlight updates when components render to see which subtree re-renders on an interaction - it is usually much larger than expected.
  • Use the flame chart to find the slowest commit, then fix that component. Optimising by intuition mostly adds memoisation to code that was never slow.
  • A long list of DOM nodes costs more than any hook: virtualise it before reaching for memo.
  • Measure production builds. Development mode is deliberately slow, and StrictMode double-invokes render to surface side effects.

Memoisation, in context

import { memo, useCallback, useMemo, useState } from 'react'

// memo skips a re-render when the props are shallow-equal
const Row = memo(function Row({ guide, onSelect }) {
  return (
    <li>
      <button type="button" onClick={() => onSelect(guide.id)}>{guide.title}</button>
    </li>
  )
})

function GuideList({ guides }) {
  const [selected, setSelected] = useState(null)

  // a stable handler keeps memo effective: a new function each render defeats it
  const onSelect = useCallback(id => setSelected(id), [])

  // a measured expensive derivation, recomputed only when its input changes
  const sorted = useMemo(() => [...guides].sort(byTitle), [guides])

  return (
    <ul>
      {sorted.map(guide => (
        <Row key={guide.id} guide={guide} onSelect={onSelect} />
      ))}
    </ul>
  )
}
ToolUse it whenCost
memoA child re-renders with equal propsA shallow comparison on every render
useMemoA calculation is measurably expensiveMemory plus a dependency comparison
useCallbackA memoised child or an effect needs a stable identityThe same as useMemo
useTransitionA state update makes the UI feel stuckDelays the update; needs a pending UI
useDeferredValueA slow list must keep up with fast typingRenders stale content briefly
VirtualisationThousands of rows are renderedA dependency and more complex markup

The React Compiler changes this calculus: it analyses components and inserts the memoisation that is actually needed, so hand-written memo and useMemo become a fallback for code that does not follow the rules of React. Enable it in the build and delete the memoisation you added defensively.

Transitions and deferred values

import { useDeferredValue, useState, useTransition } from 'react'

function Search() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [isPending, startTransition] = useTransition()
  const deferred = useDeferredValue(query)

  function handleChange(event) {
    const value = event.target.value
    setQuery(value)                       // urgent: the input must stay responsive
    startTransition(() => {               // interruptible: may be superseded
      setResults(search(value))
    })
  }

  const stale = query !== deferred

  return (
    <>
      <input value={query} onChange={handleChange} />
      <ResultList results={results} dimmed={stale || isPending} />
    </>
  )
}
  1. Reproduce the slow interaction and confirm it with the Profiler before changing anything.
  2. Keep the input's own state urgent; wrap only the expensive update in startTransition.
  3. Show a pending indicator, otherwise an interruptible update looks like a freeze.
  4. Reach for useDeferredValue when you cannot wrap the update - for example when the slow value comes from props.
  5. Only after that, check whether a smaller data structure or a virtualised list removes the need entirely.
⚠️
Memoisation is not free: it costs a comparison on every render and hides the real cost of work done inside the render body. If a list still feels slow after memoising, the problem is usually the amount of work, not the number of renders.

FAQ

Is useMemo the answer to a slow render?
Rarely. The cost is usually building a large element tree or doing work in the render body, neither of which useMemo fixes. Split the component, move the work out of render, or virtualise the list first.
What does the React Compiler change?
It inserts the memoisation the code actually needs, based on what each component reads, which removes most manual memo and useCallback calls. The code must still follow the rules of React, or it is skipped.

Custom hooks and sharing logic Testing React components

Last refreshed 2026-09-18.