React Performance: Memo, useMemo, and useCallback Guide
Optimize React performance with React.memo, useMemo, and useCallback. Learn when to use each, how to profile, and avoid common pitfalls.
What you'll learn
- ✓How React.memo prevents unnecessary re-renders
- ✓When useMemo and useCallback actually help
- ✓Profiling React apps to find real bottlenecks
- ✓Common optimization mistakes to avoid
Prerequisites
- •React components and hooks
- •Understanding of JavaScript reference equality
Performance optimization in React starts with understanding how rendering works. React re-renders a component whenever its state changes or its parent re-renders. Most of the time this is fast enough. But when it is not, React.memo, useMemo, and useCallback give you control over what gets recalculated and what gets skipped.
The catch is that these tools have costs. Using them everywhere actually makes performance worse. This guide teaches you when they help and when they hurt.
How React Rendering Works
When state changes in a component, React re-renders that component and all of its children. This does not mean the DOM updates for every child. React compares the new virtual DOM with the old one and only touches the real DOM where differences exist.
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<ExpensiveChild /> {/* Re-renders every click even though nothing changed */}
<AnotherChild /> {/* Also re-renders */}
</div>
);
}
Both children re-render when count changes, even though they do not use count. For simple components this is fine. React’s diffing is fast. But if ExpensiveChild does heavy computation or renders thousands of elements, you want to skip unnecessary re-renders.
React.memo: Skipping Re-renders
React.memo wraps a component and skips re-rendering if its props have not changed:
const ExpensiveChild = React.memo(function ExpensiveChild({ data }) {
// Only re-renders when `data` changes (shallow comparison)
return (
<ul>
{data.map(item => (
<li key={item.id}>{processItem(item)}</li>
))}
</ul>
);
});
React.memo performs a shallow comparison of props. For primitive values (strings, numbers, booleans), this works perfectly. For objects and arrays, it compares references.
function Parent() {
const [count, setCount] = useState(0);
// This object is created fresh every render - new reference each time
const style = { color: 'blue' };
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
{/* React.memo won't help here because style is a new object every render */}
<MemoizedChild style={style} />
</div>
);
}
This is where useMemo comes in.
useMemo: Caching Computed Values
useMemo caches the result of a computation between renders. It only recalculates when its dependencies change:
function ProductList({ products, filter }) {
// Only recomputes when products or filter change
const filteredProducts = useMemo(() => {
return products.filter(product =>
product.name.toLowerCase().includes(filter.toLowerCase())
);
}, [products, filter]);
return (
<ul>
{filteredProducts.map(product => (
<li key={product.id}>{product.name} - ${product.price}</li>
))}
</ul>
);
}
Use useMemo for two scenarios: expensive computations and stable references.
Expensive computations:
function Analytics({ transactions }) {
const stats = useMemo(() => {
// Imagine this processes 10,000+ transactions
const total = transactions.reduce((sum, t) => sum + t.amount, 0);
const average = total / transactions.length;
const max = Math.max(...transactions.map(t => t.amount));
const min = Math.min(...transactions.map(t => t.amount));
const sorted = [...transactions].sort((a, b) => b.amount - a.amount);
const topFive = sorted.slice(0, 5);
return { total, average, max, min, topFive };
}, [transactions]);
return (
<div>
<p>Total: ${stats.total}</p>
<p>Average: ${stats.average.toFixed(2)}</p>
</div>
);
}
Stable references for child components:
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// Stable reference - only changes when name changes
const config = useMemo(() => ({
displayName: name.toUpperCase(),
showAvatar: name.length > 0,
}), [name]);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<input value={name} onChange={e => setName(e.target.value)} />
<MemoizedProfile config={config} />
</div>
);
}
Now MemoizedProfile only re-renders when name changes, not when count changes.
useCallback: Caching Functions
useCallback is useMemo for functions. It returns the same function reference between renders unless its dependencies change:
function TodoList() {
const [todos, setTodos] = useState([]);
// Without useCallback, this is a new function every render
// MemoizedTodoItem would re-render every time
const handleDelete = useCallback((id: number) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
const handleToggle = useCallback((id: number) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
}, []);
return (
<ul>
{todos.map(todo => (
<MemoizedTodoItem
key={todo.id}
todo={todo}
onDelete={handleDelete}
onToggle={handleToggle}
/>
))}
</ul>
);
}
const MemoizedTodoItem = React.memo(function TodoItem({
todo,
onDelete,
onToggle,
}: TodoItemProps) {
return (
<li>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
});
Without useCallback, every render creates new handleDelete and handleToggle functions. Since React.memo compares props by reference, the memoized TodoItem would re-render anyway, making React.memo useless.
Profiling Before Optimizing
Never optimize without measuring first. React DevTools has a Profiler that shows you exactly which components re-render and how long they take.
import { Profiler } from 'react';
function onRenderCallback(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) {
console.log(`${id} ${phase}: ${actualDuration.toFixed(2)}ms`);
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Dashboard />
</Profiler>
);
}
Steps to profile:
- Open React DevTools and switch to the Profiler tab.
- Click “Record” and interact with your app.
- Stop recording and examine the flame chart.
- Look for components that re-render frequently with high render times.
- Only optimize those specific components.
When NOT to Optimize
Most components do not need memoization. Here are cases where adding React.memo, useMemo, or useCallback makes things worse:
Simple components that render fast:
// Don't bother memoizing this
function Label({ text }) {
return <span className="label">{text}</span>;
}
The overhead of comparing props is more expensive than just re-rendering.
Values that change every render anyway:
function BadExample({ items }) {
// Useless - items changes every render so useMemo recalculates every time
const sorted = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items] // If parent creates a new array each render, this never caches
);
}
useCallback without React.memo on the child:
function Parent() {
// Pointless - Child doesn't use React.memo so it re-renders anyway
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <Child onClick={handleClick} />;
}
Optimizing Lists
Lists are the most common performance bottleneck. When rendering hundreds of items, optimize the list item, not the list container:
const MemoizedRow = React.memo(function Row({ item, onSelect }) {
return (
<tr onClick={() => onSelect(item.id)}>
<td>{item.name}</td>
<td>{item.email}</td>
<td>{item.role}</td>
</tr>
);
});
function DataTable({ data }) {
const [selectedId, setSelectedId] = useState(null);
const handleSelect = useCallback((id: number) => {
setSelectedId(id);
}, []);
return (
<table>
<tbody>
{data.map(item => (
<MemoizedRow
key={item.id}
item={item}
onSelect={handleSelect}
/>
))}
</tbody>
</table>
);
}
For very large lists (1000+ items), consider virtualization with libraries like @tanstack/react-virtual instead of memoization. Virtualization only renders items visible in the viewport.
State Structure Optimization
Sometimes the best optimization is restructuring your state to reduce the blast radius of updates:
// Bad: updating searchQuery re-renders everything including the heavy table
function Page() {
const [searchQuery, setSearchQuery] = useState('');
const [data, setData] = useState([]);
return (
<div>
<SearchInput value={searchQuery} onChange={setSearchQuery} />
<HeavyTable data={data} />
</div>
);
}
// Better: move searchQuery state into SearchInput
function Page() {
const [data, setData] = useState([]);
return (
<div>
<SearchInput onSearch={handleSearch} />
<HeavyTable data={data} />
</div>
);
}
function SearchInput({ onSearch }) {
const [query, setQuery] = useState('');
return (
<input
value={query}
onChange={e => {
setQuery(e.target.value);
onSearch(e.target.value);
}}
/>
);
}
By moving searchQuery state down into SearchInput, typing no longer re-renders HeavyTable. This is called “state colocation” and it is often more effective than memoization.
A Decision Framework
Follow this checklist when you suspect a performance issue:
- Profile first. Use React DevTools to confirm there is a real problem.
- Try state colocation. Move state closer to where it is used.
- Use
React.memoon components that re-render often with the same props. - Use
useCallbackon functions passed to memoized children. - Use
useMemofor expensive computations or stable object/array references. - Consider virtualization for long lists.
Wrapping Up
React’s memoization tools are powerful but should be applied surgically. Profile your app to find actual bottlenecks, try restructuring state first, and reach for React.memo, useMemo, and useCallback only when you have evidence they will help. Remember that every optimization adds complexity and has a memory cost. The fastest code is often the simplest code that React can render efficiently on its own.
Related articles
- React React useTransition: Advanced Patterns for Responsive UIs
Go beyond basic useTransition usage with patterns for search, navigation, tab switching, and priority-based rendering in React concurrent mode.
- React React Compiler and Automatic Memoization
Understand how the React Compiler works, what automatic memoization means, and how it eliminates the need for useMemo, useCallback, and React.memo.
- React React Concurrent Features: useTransition, useDeferredValue, and More
Master React's concurrent rendering features including useTransition, useDeferredValue, and strategies for keeping your UI responsive.
- React React Context Performance: Splitting, Memoizing, and Selectors
Solve React Context performance problems with provider splitting, value memoization, selector patterns, and smart composition strategies.