Performance: memo, useMemo, useCallback
First — don't fear re-renders
You already know that a component re-renders when its state changes and when its parent renders. Beginners often panic: "everything re-renders at the slightest sneeze!". But rendering is a function call and an object comparison, usually a fraction of a millisecond. The real DOM changes only where there's a difference. In most applications nothing needs to be optimized.
Optimization is needed when there's a noticeable problem: an input field "lags", a list of thousands of rows
re-renders entirely, a heavy calculation runs somewhere. Then React has three tools: useMemo, memo and useCallback.
You definitely need to understand them — they're in any real project — and use them wisely.
useMemo: remember the result of a calculation
const { useMemo } = React;
function ProductList({ products, query }) {
const [dark, setDark] = useState(false);
const visible = useMemo(
() => products.filter((p) => p.title.toLowerCase().includes(query.toLowerCase())),
[products, query],
);
return (
<div className={dark ? "dark" : ""}>
<button onClick={() => setDark(!dark)}>Theme</button>
<ul>{visible.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
</div>
);
}
useMemo(function, [dependencies])calls the function on the first render and remembers the result.- On the following renders, if the dependencies haven't changed, it returns the remembered value right away without calling the function.
- Switching the theme no longer filters the list again:
productsandqueryare the same.
For a filter over a hundred products you don't need it — it's fast anyway. useMemo is needed when the calculation is
really heavy (thousands of elements, complex math) or when it's important to keep the same reference to an object
(more on that below).
memo: skip a component's render
memo wraps a component and tells React: "if the props are the same as last time, don't call me — use the previous result":
const { memo } = React;
const UserCard = memo(function UserCard({ name, avatar }) {
console.log("render", name);
return (
<div className="card">
<img src={avatar} alt="" />
<p>{name}</p>
</div>
);
});
Now when the parent renders, UserCard re-renders only if name or avatar changed. Its own state and context changes
re-render the component as usual — memo only looks at props.
The trap: comparing by reference
memo compares each prop with Object.is — for objects, arrays and functions that's a comparison of references. And
in the component body new objects and functions are created on every render:
function Parent() {
const [n, setN] = useState(0);
const style = { color: "red" }; // a new object every time
const handleClick = () => console.log(1); // a new function every time
return (
<>
<button onClick={() => setN(n + 1)}>{n}</button>
<Child style={style} onClick={handleClick} /> {/* memo won't help! */}
</>
);
}
For memo these are "new props" every time, and Child re-renders anyway. For memo to work, the objects and functions
in props must keep their reference between renders:
- constants — move them outside the component;
- computed objects and arrays —
useMemo; - functions —
useCallback.
useCallback: a stable function
const { useCallback } = React;
function TodoList() {
const [todos, setTodos] = useState([]);
const handleDelete = useCallback((id) => {
setTodos((list) => list.filter((t) => t.id !== id));
}, []);
return todos.map((t) => <TodoItem key={t.id} todo={t} onDelete={handleDelete} />);
}
const TodoItem = memo(function TodoItem({ todo, onDelete }) {
return <li onClick={() => onDelete(todo.id)}>{todo.text}</li>;
});
useCallback(function, [dependencies]) returns the very same function as long as the dependencies haven't changed. In
essence it's useMemo(() => function, dependencies).
Note setTodos((list) => …). If there were todos.filter(…) inside, todos would have to be added to the dependencies —
and the function would change on every change of the list. The updater function takes the fresh list from its argument, so
there are no dependencies and the function is stable forever.
How to find what's slow
Don't guess — measure:
- React Developer Tools (a browser extension) → the Profiler tab: record an action and see which components re-rendered and how long it took;
- in its settings — "Highlight updates": re-rendered components are highlighted on the page;
console.login the component body — the simplest render counter.
Common real causes of slowness: very long lists (the fix is to show only the visible part, "virtualization"), heavy calculations on every render, state too high in the tree (an input field at the root of the application re-renders the whole application — move the state down to where it's needed).
When NOT to optimize
useMemoanduseCallbackthemselves cost time and memory and complicate the code. Wrapping everything in them is harmful.useCallbackwithoutmemoon the child component is almost useless: if the child isn't memoized, it re-renders anyway.- First — the right structure: state as low as possible, calculations during rendering, no extra effects. That solves most
problems without a single
memo.
Newer versions of React are getting the React Compiler, which adds such memoization automatically. But you still need to understand what it does and why.
Summary
- Rendering is usually cheap. Optimize when there's a measured problem.
useMemo(() => calculation, [deps])— remember the result of a heavy calculation or an object.memo(Component)— skip the render when the props are the same.- Props are compared by reference: new objects and functions break
memo. useCallback(fn, [deps])— the same function between renders; together withsetX((old) => …)— without extra dependencies.- Measure with the React DevTools Profiler.