Introduction
React is fast by default because of the virtual DOM, but as applications grow, unnecessary re-renders can slow things down. Performance optimization in React mainly revolves around avoiding wasted work: skipping re-renders of components whose output hasn't changed, and avoiding expensive recalculations on every render. The three main tools for this are React.memo (to memoize components), useMemo (to memoize computed values), and useCallback (to memoize function references).
Cricket analogy: A team is fast by default with a solid top order, but repeatedly re-reviewing every routine single on DRS wastes time; smart captains skip reviewing unchanged decisions (React.memo), pre-calculate the required run rate once per over (useMemo), and reuse the same fixed field signal (useCallback).
Syntax
import React, { useMemo, useCallback } from 'react';
// Memoize a component so it only re-renders when its props change
const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) {
console.log('Rendering ExpensiveList');
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
function Parent({ items }) {
// Memoize an expensive derived value
const sortedItems = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// Memoize a callback so it keeps the same reference across renders
const handleSelect = useCallback((id) => {
console.log('Selected item', id);
}, []);
return <ExpensiveList items={sortedItems} onSelect={handleSelect} />;
}Explanation
React.memo wraps a component and performs a shallow comparison of its props between renders; if none of the props changed, React skips re-rendering that component and reuses the last rendered output. useMemo caches the result of a computation between renders and only recalculates it when one of its dependencies changes, which is useful for expensive operations like sorting, filtering, or heavy math. useCallback is similar but caches a function reference instead of a value — this matters because in JavaScript a new function is created on every render, and passing a 'new' function as a prop to a React.memo'd child would defeat the memoization since the prop reference always changes. Together, useMemo and useCallback let memoized children actually skip re-rendering.
Cricket analogy: React.memo is the coach checking if a fielder's assigned position actually changed before repositioning them; useMemo caches the pre-calculated required run rate instead of recomputing it every ball; useCallback keeps the same fixed hand-signal reference so the memoized fielder doesn't reposition just because a "new" signal object was flashed each ball.
Do not wrap every component in React.memo or every value in useMemo/useCallback by default. Memoization itself has a cost (extra memory and comparison work), and for cheap components or calculations it can make performance worse. Profile first with the React DevTools Profiler, then optimize the components that actually re-render expensively and frequently.
Example
function App() {
const [count, setCount] = useState(0);
const [items] = useState([
{ id: 1, name: 'Banana' },
{ id: 2, name: 'Apple' },
]);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>
Clicked {count} times
</button>
{/* Without React.memo, ExpensiveList would re-render on every click */}
<Parent items={items} />
</div>
);
}Output
Clicking the button increments count and re-renders App and Parent, but because items never changes reference and handleSelect is memoized with useCallback, ExpensiveList (wrapped in React.memo) does not re-render — the 'Rendering ExpensiveList' log only appears once, on the initial mount, instead of on every click.
Cricket analogy: Clicking the scoreboard's manual run-adjust button re-renders the whole control panel, but since the fixed fielding chart's reference never changes and the signal callback is memoized, the "repositioning fielders" log only fires once at the start, not on every adjustment.
Key Takeaways
- React.memo skips re-rendering a component when its props are shallowly equal to the previous render.
- useMemo caches an expensive computed value and recalculates it only when its dependencies change.
- useCallback caches a function reference so it doesn't change identity on every render.
- Memoization only helps when paired correctly: memoized children need memoized callback/object props to actually skip re-renders.
- Always measure with the React DevTools Profiler before adding memoization — premature optimization can add unnecessary complexity.
Practice what you learned
1. What does React.memo do to a functional component?
2. Why is useCallback often used together with React.memo?
3. What is a valid concern about overusing useMemo and useCallback?
4. What triggers useMemo to recompute its cached value?
Was this page helpful?
You May Also Like
useMemo and useCallback
Learn how useMemo and useCallback memoize values and functions between renders, and when that memoization is actually worthwhile.
Code Splitting and Lazy Loading
Reduce initial bundle size in React apps using React.lazy and Suspense to load components on demand.
Virtual DOM and Reconciliation
Understand how React's Virtual DOM and reconciliation algorithm efficiently update the real browser DOM.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics