- Home
- /
- Tutorials
- /
- React Tutorial
- /
- Memoisation in React
Performance
Memoisation in React
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.
- Notice something actually feels slow.
- Profile it - React DevTools shows which components render and how long they take.
- Fix the real cause, which is often an unnecessary state update rather than a slow render.
- 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.
