Learn React's performance optimization tools: React.memo for preventing unnecessary component re-renders, useMemo for caching expensive computations, and useCallback for stable function references.
Before learning optimization tools, you need to understand when and why React re-renders components.
A component re-renders when:
setState or dispatch triggers a re-render.The second rule is the one that surprises most developers. Consider this example:
function Parent() {
const [count, setCount] = React.useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<ExpensiveChild name="Alice" /> {/* re-renders on every count change! */}
</div>
);
}
function ExpensiveChild({ name }) {
// Imagine this does heavy computation or renders a long list
console.log('ExpensiveChild rendered');
return <p>Hello, {name}!</p>;
}Every time the parent's count changes, ExpensiveChild re-renders even though its name prop ("Alice") never changes. For simple components, this is fine — React's diffing is fast. But for components that are genuinely expensive (long lists, complex calculations, heavy DOM trees), these unnecessary re-renders can cause noticeable slowdowns.
React provides three tools to optimize this:
React.memo — skip re-rendering a component when its props have not changeduseMemo — cache the result of an expensive computationuseCallback — cache a function reference so it does not change on every renderReact.memo is a higher-order component that wraps your component and skips re-rendering if its props have not changed (using shallow comparison).
const ExpensiveChild = React.memo(function ExpensiveChild({ name }) {
console.log('ExpensiveChild rendered');
return <p>Hello, {name}!</p>;
});Now ExpensiveChild will only re-render when name actually changes. When the parent re-renders due to a count update, React compares the old props ({ name: 'Alice' }) with the new props ({ name: 'Alice' }). Since they are the same (shallow equal), React skips rendering ExpensiveChild entirely.
React.memo uses shallow comparison by default. This means:
'Alice' === 'Alice' is true.{} === {} is false because they are different objects in memory.This is important! If you pass an object or function as a prop, a new reference is created on every render:
function Parent() {
const [count, setCount] = React.useState(0);
// NEW object every render — defeats React.memo!
const style = { color: 'red' };
// NEW function every render — also defeats React.memo!
const handleClick = () => console.log('clicked');
return <MemoChild style={style} onClick={handleClick} />;
}Even though the values are logically the same, they are new references each time, so React.memo sees them as "changed" and re-renders anyway. This is where useMemo and useCallback come in.
You can pass a custom comparison function as the second argument to React.memo:
const MemoChild = React.memo(MyComponent, (prevProps, nextProps) => {
return prevProps.id === nextProps.id; // only re-render if id changes
});Return true to skip re-rendering, false to allow it.
useMemo takes a function and a dependency array, and returns the memoized result. React only re-runs the function when one of the dependencies changes.
const memoizedValue = React.useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]);1. Expensive calculations:
function FilteredList({ items, filter }) {
// Without useMemo: runs on EVERY render
// With useMemo: runs only when items or filter change
const filtered = React.useMemo(() => {
console.log('Filtering...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
<ul>
{filtered.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}2. Stabilizing object/array references for React.memo children:
function Parent() {
const [count, setCount] = React.useState(0);
// Without useMemo: new array every render
// With useMemo: same reference unless data changes
const items = React.useMemo(() => ['Apple', 'Banana', 'Cherry'], []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<MemoizedList items={items} />
</div>
);
}Because items has a stable reference (same array object), the memoized child will correctly skip re-rendering when count changes.
On the first render, React calls the function and stores both the result and the dependencies. On subsequent renders, React compares the current dependencies with the stored ones. If they are the same (shallow comparison), React returns the stored result without calling the function again. If they differ, React calls the function and stores the new result.
useCallback is essentially useMemo for functions. It returns a memoized version of the callback that only changes when its dependencies change.
const memoizedFn = React.useCallback(() => {
doSomething(a, b);
}, [a, b]);This is equivalent to:
const memoizedFn = React.useMemo(() => {
return () => doSomething(a, b);
}, [a, b]);In JavaScript, every function expression creates a new function object:
function Parent() {
const [count, setCount] = React.useState(0);
// Without useCallback: new function every render
const handleClick = () => console.log('clicked');
// With useCallback: same function reference across renders
const handleClickStable = React.useCallback(() => {
console.log('clicked');
}, []);
return <MemoChild onClick={handleClickStable} />;
}Without useCallback, handleClick is a new function on every render, which means React.memo on MemoChild would be useless — it would always see a "new" onClick prop.
With useCallback, handleClickStable is the same function reference across renders (as long as its dependencies do not change), so React.memo can correctly skip re-rendering.
Use useCallback when:
React.memo)useEffect, useMemo, or another hookDo NOT use useCallback everywhere. It adds complexity and memory overhead. Only use it when there is a measurable benefit.
The most important performance optimization concept is knowing when NOT to optimize. Premature optimization is a common trap in React development.
React's rendering is very fast. For most components, the cost of re-rendering is negligible (microseconds). Adding React.memo, useMemo, and useCallback everywhere has its own costs:
Before reaching for optimization tools, ask yourself:
<p> tags does not need React.memo.React DevTools includes a Profiler tab that records renders:
Look for components that:
React.memo)Profile first, then optimize the bottlenecks. Never guess where the performance problem is.
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
// Memoized child — only re-renders when items change
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
console.log("ExpensiveList rendered");
return (
<ul id="list">
{items.map((item, i) => <li key={i}>{item}</li>)}
</ul>
);
});
function App() {
const [count, setCount] = React.useState(0);
// useMemo: stable array reference
const items = React.useMemo(() => ["React", "Vue", "Angular"], []);
// useCallback: stable function reference
const increment = React.useCallback(() => {
setCount(c => c + 1);
}, []);
return (
<div>
<h2>Performance Demo</h2>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<p id="render-info">Items: {items.length}</p>
<ExpensiveList items={items} />
<p><em>Open console — ExpensiveList does not re-render on count change!</em></p>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>What is the main risk of using React.memo, useMemo, and useCallback everywhere?