The vast majority of React performance problems come not from bad algorithms but from needless re-renders. Every time a component renders, it re-runs its subtree; usually that is cheap, but with large lists, heavy computations or deep trees the UI starts to stutter. This article shows how to measure renders and how to cut them sensibly with the trio of memo, useMemo and useCallback.
Why does React re-render?
A component re-renders in three cases: when its own state changes, when the props it receives change, or when its parent re-renders. That last point is the one most often missed: when a parent renders, its children re-render by default even if their props never changed.
Most of the time this is fine; React's render is plain JavaScript execution and it only touches the real DOM when there is an actual difference. The trouble begins when the render itself is expensive: a table with thousands of rows, a complex chart, or a heavy loop recomputed on every render.
Measure before you optimize
Premature optimization is the biggest trap. Before sprinkling memo everywhere, measure where it is actually slow. The Profiler tab in the React Developer Tools extension is made exactly for this: start recording, perform the interaction, stop. The flame graph shows which component rendered how many times and for how many milliseconds.
During development, turn on the Profiler's "Record why each component rendered" option. If you frequently see the render reason "Parent component rendered", you have found a candidate for memoization.
React.memo to remember a component
React.memo wraps a component and skips the render if its props have not changed by a shallow comparison. It is ideal for pure, props-driven presentational components.
import { memo } from 'react';
const UserRow = memo(function UserRow({ name, email }) {
console.log('render', name);
return (
<tr>
<td>{name}</td>
<td>{email}</td>
</tr>
);
});
Now UserRow will not re-render when the parent renders, as long as name and email stay the same. But beware: the shallow comparison compares object and function props by reference. If you create a new object or function on every render, memo does nothing, because the reference changes each time.
useMemo to cache expensive computations
useMemo remembers a value: it does not re-run the computation unless the dependencies you pass change. It has two main uses: skipping genuinely expensive computations, and keeping a stable object/array reference to hand to memo-wrapped children.
const sortedUsers = useMemo(() => {
return [...users].sort((a, b) => a.name.localeCompare(b.name));
}, [users]);
Here the sort only runs when the users array changes; when some other state in the parent triggers a render, the sort is skipped. Key rule: be honest with the dependency array. Every external variable used inside the computation must be in the array, otherwise you work with stale values.
Do not give in to the temptation to wrap everything in useMemo. The comparison and the cache themselves cost something; memoizing a cheap calculation like a + b is a net loss.
useCallback to stabilize a function reference
useCallback is really the function-specific form of useMemo: it preserves a function's reference until the dependencies change. Its real value appears when you pass a callback to a memo-wrapped child.
const handleDelete = useCallback((id) => {
setUsers((prev) => prev.filter((u) => u.id !== id));
}, []);
// the memo'd child no longer re-renders because of this prop
<UserRow user={user} onDelete={handleDelete} />
With an empty dependency array, the function is created once on the first render and keeps the same reference forever. Notice that I use the functional (updater) form of setUsers — that way I avoid adding users to the dependencies and keep the callback stable.
Common mistakes
- Passing a new object literal to a memo'd component:
style={{ margin: 8 }}is a new reference on every render and defeats memoization. Move the constant object outside the component or build it withuseMemo. - Using the index as a list key: in lists with sorting or insertion/deletion,
key={index}causes wrong matches and needless renders; use a stable id. - Sprinkling optimization everywhere:
memo/useMemoadded without measuring hurts readability and gives no benefit.
A note on React 19 and the compiler
The React Compiler introduced around React 19 aims to do most of this memoization automatically at build time; in other words, the future need to hand-write useMemo/useCallback may shrink. Still, the compiler is not yet enabled in every project, and understanding these concepts remains essential for existing codebases. The principle "measure first, then think, optimize last" holds in every version.
Frequently Asked Questions
What is the difference between memo, useMemo and useCallback?
memo wraps a component and skips its render unless props change. useMemo caches a value. useCallback keeps a function reference stable; it is really shorthand for useMemo(() => fn, deps).
Should I wrap every component in memo?
No. The comparison also has a cost and is a net loss for cheap renders. Only memoize components you have proven with the Profiler to be genuinely expensive or to render very often.
Does useMemo really make things faster?
Only if the computation is expensive enough. For simple arithmetic or small arrays the cost of the cache can outweigh the benefit. Measure and decide.
Want to speed up your React app? To profile your renders and fix bottlenecks with real measurements, get in touch with me.