Performance Optimization
React is fast by default. But as your app grows â more components, bigger lists, heavier computations â you'll start noticing slowdowns. This chapter teaches you how to find and fix performance problems, and equally important, when to leave things alone.
Why Does React Re-render?
Before you can fix unnecessary re-renders, you need to understand what triggers them. There are exactly three reasons a component re-renders:
- Its own state changes â you called
setStateordispatch - Its parent re-renders â even if the child's props didn't change
- A context it consumes changes â any component using
useContextre-renders when the context value changes
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<p>Parent count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<Child name="Alice" />
</div>
);
}
function Child({ name }) {
console.log('Child rendered!');
return <p>Hello, {name}</p>;
}Every time you click the button, Child re-renders â even though name never changes. Why? Because its parent re-rendered, and by default, all children re-render too.
Think of it like: When a manager calls an all-hands meeting (parent re-renders), every team member has to attend (children re-render), even if the meeting has nothing to do with them.
How to Prevent Unnecessary Re-renders
The first and best strategy is structural â organize your components so re-renders don't cascade:
Strategy 1: Move State Down
If only one part of the UI needs a piece of state, keep that state in the smallest possible component:
// Bad: Search state lives too high
function App() {
const [search, setSearch] = useState('');
return (
<div>
<SearchBar value={search} onChange={setSearch} />
<ExpensiveProductList />
</div>
);
}
// Good: Search state lives where it's used
function App() {
return (
<div>
<SearchSection />
<ExpensiveProductList />
</div>
);
}
function SearchSection() {
const [search, setSearch] = useState('');
return <SearchBar value={search} onChange={setSearch} />;
}Now when the user types, only SearchSection re-renders. ExpensiveProductList is untouched.
Strategy 2: Lift Content Up (Children Pattern)
Pass expensive components as children so they don't re-render when the parent's state changes:
function ScrollTracker({ children }) {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handler = () => setScrollY(window.scrollY);
window.addEventListener('scroll', handler);
return () => window.removeEventListener('scroll', handler);
}, []);
return (
<div>
<p>Scroll position: {scrollY}</p>
{children}
</div>
);
}
// Usage â ExpensiveList doesn't re-render on scroll!
function Page() {
return (
<ScrollTracker>
<ExpensiveList />
</ScrollTracker>
);
}The children JSX is created by the parent of ScrollTracker (which is Page). Since Page doesn't re-render on scroll, the children reference stays the same, and ExpensiveList skips re-rendering.
Remember: Before reaching for
React.memo, try reorganizing your component tree. Moving state down and lifting content up solve most performance problems with zero overhead.
React.memo â The "Skip Re-render" Wrapper
React.memo wraps a component and tells React: "Only re-render this if its props actually changed."
const UserCard = React.memo(function UserCard({ name, email }) {
console.log('UserCard rendered');
return (
<div>
<h3>{name}</h3>
<p>{email}</p>
</div>
);
});Now even if the parent re-renders, UserCard will skip re-rendering as long as name and email are the same.
When to Use React.memo
- The component's parent re-renders frequently
- The component is expensive to render (large list, heavy computations)
- The component's props don't change often
When NOT to Use React.memo
- The component is cheap to render (a simple
<p>or<span>) â the memo comparison itself costs more than just re-rendering - The component always receives new props â memo checks on every render but always fails, adding overhead for nothing
- The component's parent rarely re-renders â there's nothing to prevent
Common Mistake: Wrapping everything in
React.memo. Memoization isn't free â React has to compare all props on every render. For simple components, this comparison is slower than just re-rendering.
Interview Q&A:
Q: Does React.memo do a deep comparison of props?
No, it does a shallow comparison by default. It checks if each prop is the same reference using ===. This means { color: 'red' } !== { color: 'red' } because they're different objects in memory. You can pass a custom comparison function as the second argument, but this is rarely needed.
useMemo â Caching Expensive Calculations
useMemo lets you cache the result of a calculation so it only re-runs when its dependencies change.
Think of it like a recipe with leftovers. If you cooked lasagna yesterday and nobody ate it, you don't cook a fresh one today â you serve the leftovers. You only cook again when the ingredients change or the leftovers run out.
function FilteredTodoList({ todos, filter }) {
const filteredTodos = useMemo(() => {
return todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
}, [todos, filter]);
return (
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}Without useMemo, the filter() runs on every re-render â even if todos and filter haven't changed. With useMemo, React says: "Same inputs? Here are the cached results."
When to Use useMemo
- The calculation is genuinely expensive (filtering thousands of items, complex transformations)
- You need a stable reference to an object or array (for passing to a memoized child component)
// Stable reference for a memoized child
function Parent() {
const [count, setCount] = useState(0);
const config = useMemo(() => ({
theme: 'dark',
size: 'large',
}), []);
return <MemoizedChild config={config} />;
}useCallback â Keeping Function References Stable
useCallback is useMemo for functions. It returns the same function reference unless its dependencies change.
Why does this matter? Every time a component renders, it creates new function instances:
function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => console.log('clicked');
return <MemoizedChild onClick={handleClick} />;
}Even though handleClick does the same thing every time, it's a brand new function object on each render. So React.memo on MemoizedChild fails â it sees onClick as "changed."
The fix:
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <MemoizedChild onClick={handleClick} />;
}Now handleClick is the same reference across renders, and React.memo works correctly.
Think of it like: Imagine giving someone your phone number. If you write it on a new piece of paper every time, they'd think it's a different number.
useCallbackis like giving them the same business card every time â same number, same card.
What Is Memoization?
Memoization is a fancy word for a simple idea: remember the result of a computation so you don't have to redo it.
Think of it like a kitchen shortcut. If a customer orders the same dish you just made, you don't start cooking from scratch â you serve from what you already prepared. You only cook fresh when a new, different order comes in.
In React, memoization shows up in three places:
| Tool | What It Memoizes | Purpose |
|---|---|---|
React.memo | A component's rendered output | Skip re-rendering if props are the same |
useMemo | A computed value | Skip recalculating if dependencies are the same |
useCallback | A function reference | Keep the same function identity across renders |
They all follow the same pattern:
- First time: Do the work, store the result
- Next time, same inputs: Return the stored result
- Next time, different inputs: Redo the work, store the new result
When NOT to Memoize â The Overhead Trap
Memoization has a cost:
- Memory â React stores the previous result
- Comparison â React compares dependencies on every render
- Complexity â More code to read and maintain
// Don't do this â the memo costs more than the computation
const greeting = useMemo(() => `Hello, ${name}!`, [name]);
// Just compute it directly
const greeting = `Hello, ${name}!`;The rule of thumb: Don't memoize unless you've measured a performance problem. Premature memoization adds complexity for zero benefit.
// Don't do this â wrapping every component in React.memo
const Title = React.memo(({ text }) => <h1>{text}</h1>);
// Just render it â it's a single DOM node, blazing fast already
const Title = ({ text }) => <h1>{text}</h1>;Think of it like: Putting every single item in your fridge in a separate labeled container. Sure, it's organized, but the time spent labeling and finding containers is more than the time you save. Only containerize the expensive stuff.
When memoization is worth it:
- Filtering/sorting thousands of items
- Components that render large subtrees (tables, charts, dashboards)
- Functions passed as props to memoized children
When memoization is NOT worth it:
- Simple string concatenation or math
- Components that render a few DOM nodes
- Components whose parent rarely re-renders anyway
Remember: Profile first, optimize second. React DevTools has a Profiler tab that shows which components rendered and how long they took. Use it before adding any memoization.
Virtualization â Only Render What's Visible
Imagine a restaurant menu with 10,000 dishes. You wouldn't print all 10,000 on one giant scroll â you'd show one page at a time. That's virtualization.
When you have a long list (hundreds or thousands of items), rendering every single item into the DOM is expensive â even if the user can only see 10 at a time. Virtualization means you only create DOM nodes for the items currently visible on screen.
import { FixedSizeList } from 'react-window';
function VirtualTodoList({ todos }) {
const Row = ({ index, style }) => (
<div style={style}>
{todos[index].text}
</div>
);
return (
<FixedSizeList
height={400}
width="100%"
itemCount={todos.length}
itemSize={50}
>
{Row}
</FixedSizeList>
);
}Even with 10,000 todos, only about 8-10 <div> elements exist in the DOM at any time. As the user scrolls, react-window creates new DOM nodes for items coming into view and removes nodes for items leaving view.
When to Virtualize
| List Size | Action |
|---|---|
| Under 100 items | Don't bother â just render them all |
| 100â500 items | Consider it if items are complex (images, nested components) |
| 500+ items | Definitely virtualize |
| 10,000+ items | Virtualization is essential |
Virtualization + Infinite Scroll
Virtualization pairs naturally with infinite scroll â load more data as the user scrolls near the bottom:
import { FixedSizeList } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
function InfiniteUserList({ users, hasMore, loadMore }) {
const isItemLoaded = (index) => index < users.length;
return (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={hasMore ? users.length + 1 : users.length}
loadMoreItems={loadMore}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={500}
itemCount={hasMore ? users.length + 1 : users.length}
itemSize={60}
onItemsRendered={onItemsRendered}
ref={ref}
>
{({ index, style }) => (
<div style={style}>
{isItemLoaded(index) ? (
<UserRow user={users[index]} />
) : (
<p>Loading...</p>
)}
</div>
)}
</FixedSizeList>
)}
</InfiniteLoader>
);
}The user sees a smooth, scrollable list. Behind the scenes, only visible items are in the DOM, and new data loads automatically as they scroll.
Common Mistake: Trying to optimize a list with
React.memoon each item when you actually need virtualization. If you have 5,000 items, memo helps each item re-render faster, but virtualization means you only render ~10 items total. That's a much bigger win.
Performance Optimization Checklist
Before you optimize anything, ask these questions in order:
- Is there actually a problem? Use React DevTools Profiler to measure
- Can I move state down? Keep state close to where it's used
- Can I lift content up? Pass expensive components as children
- Is a long list the bottleneck? Use virtualization
- Is an expensive calculation repeated? Use
useMemo - Is a memoized child re-rendering due to function props? Use
useCallback - Is a large subtree re-rendering due to parent? Use
React.memo
Remember: The best optimization is the one you don't need to write. Good component architecture prevents most performance problems before they start.
Chapter Summary
| Tool | What It Does | Analogy |
|---|---|---|
| Move state down | Prevents cascading re-renders | Keep meetings small â don't invite the whole company |
| React.memo | Skips re-render if props unchanged | "Skip this meeting if nothing relevant changed" |
| useMemo | Caches expensive computation results | Don't re-cook the same dish â serve leftovers |
| useCallback | Keeps function reference stable | Same business card, not a new one each time |
| Virtualization | Only renders visible list items | Show one menu page, not all 10,000 dishes |