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>
)
}| Tool | Use it when | Cost |
|---|---|---|
memo | A child re-renders with equal props | A shallow comparison on every render |
useMemo | A calculation is measurably expensive | Memory plus a dependency comparison |
useCallback | A memoised child or an effect needs a stable identity | The same as useMemo |
useTransition | A state update makes the UI feel stuck | Delays the update; needs a pending UI |
useDeferredValue | A slow list must keep up with fast typing | Renders stale content briefly |
| Virtualisation | Thousands of rows are rendered | A 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} />
</>
)
}- Reproduce the slow interaction and confirm it with the Profiler before changing anything.
- Keep the input's own state urgent; wrap only the expensive update in
startTransition. - Show a pending indicator, otherwise an interruptible update looks like a freeze.
- Reach for
useDeferredValuewhen you cannot wrap the update - for example when the slow value comes from props. - 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.Related
Custom hooks and sharing logic Testing React components
Last refreshed 2026-09-18.