Performance Optimization
Understand why components re-render unnecessarily and pull together React.memo, useMemo, useCallback, code splitting, and list virtualization into one performance toolkit.
Introduction
React is fast by default for the vast majority of apps, and most components never need any special performance treatment at all. But as an app grows — more components, bigger lists, more frequent state updates — it is common to eventually run into a screen that feels sluggish. This lesson is about recognizing why that happens and knowing which tool actually fixes it.
You have already met several performance-related tools across earlier lessons in isolation: React.memo, useMemo, useCallback, and code splitting with lazy loading. This lesson ties them together into a single mental model, adds list virtualization as one more technique, and — most importantly — establishes the habit of measuring before reaching for any of them.
- Why components sometimes re-render even when nothing visible changed.
- How React.memo, useMemo, useCallback, and code splitting fit together as a toolkit.
- How virtualization keeps very long lists fast by only rendering visible rows.
- Why measuring first, rather than guessing, is the most important optimization habit of all.
Why Components Re-Render
A React component re-renders whenever its state changes, whenever its parent re-renders (by default, regardless of whether the props actually changed), or whenever context it subscribes to changes. Re-rendering itself is cheap for most components — it is just running the function again and diffing the result. Performance problems show up when a re-render is expensive (heavy computation, a huge list) and happens far more often than necessary.
Parent Re-Renders
By default, when a parent re-renders, every child re-renders too, even if that child's own props did not change.
Expensive Calculations
A component that recalculates something costly on every render feels slow even if the render count is normal.
Large Lists
Rendering hundreds or thousands of DOM nodes at once is expensive regardless of how efficient each individual item is.
Context Changes
Every component reading a piece of context re-renders whenever that context value changes, even far down the tree.
Recap: The Optimization Toolkit
Each of these tools targets a different cause of wasted work. Knowing which problem each one solves matters more than memorizing the syntax.
React.memo
Wraps a component so it skips re-rendering when its props are shallowly equal to the previous render — useful for child components that re-render often but rarely change visually.
useMemo
Caches the result of an expensive calculation between renders, recomputing only when its dependencies change.
useCallback
Caches a function reference between renders, which matters most when that function is a prop passed to a React.memo child — otherwise a new function reference would break the memoization.
Code Splitting & Lazy Loading
Splits the app's JavaScript into smaller chunks loaded on demand with React.lazy and Suspense, shrinking the initial bundle and speeding up first load.
React.memo, useMemo, and useCallback are frequently used as a set: memoize a child component with React.memo, then use useCallback for any function props and useMemo for any object or array props, so the memoized child actually gets to skip re-rendering.
Example: Memoizing an Expensive List
Consider a parent that re-renders frequently (say, on every keystroke in an unrelated search box) alongside a child that renders a large, computationally expensive list. Without memoization, that list recalculates and re-renders on every keystroke, even though its own data has not changed.
function Dashboard() {
const [search, setSearch] = useState("");
const items = useProductList(); // large array
return (
<div>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
<ExpensiveList items={items} />
</div>
);
}
function ExpensiveList({ items }) {
const sorted = items.slice().sort(expensiveCompare);
return (
<ul>
{sorted.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}const ExpensiveList = React.memo(function ExpensiveList({ items }) {
const sorted = useMemo(() => items.slice().sort(expensiveCompare), [items]);
return (
<ul>
{sorted.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});Typing in the search box no longer re-sorts or re-renders ExpensiveList,
because its "items" prop reference has not changed.Virtualizing Long Lists
Memoization helps avoid unnecessary re-renders, but a list with thousands of rows is expensive to render even once, since React still has to create and manage a DOM node for every single item. Virtualization solves this differently: instead of rendering every row, it only renders the handful of rows currently visible in the scrollable viewport, recycling DOM nodes as the user scrolls.
import { FixedSizeList } from "react-window";
function Row({ index, style, data }) {
return <div style={style}>{data[index].name}</div>;
}
function VirtualizedList({ items }) {
return (
<FixedSizeList height={400} width={300} itemCount={items.length} itemSize={35} itemData={items}>
{Row}
</FixedSizeList>
);
}Virtualization is worth the added complexity mainly for very long lists — think hundreds or thousands of rows, like a chat history or a spreadsheet. For a list of a few dozen items, plain rendering (with React.memo on the row component) is usually simpler and plenty fast.
Measure Before You Optimize
It is tempting to sprinkle React.memo and useMemo everywhere "just in case," but every one of these tools has a small cost of its own (extra memory for cached values, extra comparison work), and applying them to a component that was never actually slow can make code harder to read for no benefit.
The most reliable way to know whether a component is genuinely slow, and why, is to measure it — not to guess based on how the code looks. React's Profiler tool records exactly which components re-rendered, how long each render took, and why. The next lesson covers the React Developer Tools and Profiler in detail, and it is the natural next step before applying any of the techniques in this lesson.
Common Mistakes
- Wrapping every component in React.memo without evidence it re-renders unnecessarily or expensively.
- Using useCallback and useMemo everywhere "for performance" when the component was never measured to be slow.
- Reaching for virtualization on a short list where it adds complexity without a real speed benefit.
- Optimizing based on a hunch instead of profiling data, which often targets the wrong component entirely.
- Forgetting that React.memo does nothing if new object or function props are created on every parent render.
Best Practices
- Start with plain components; add memoization only when a real, measured slowdown appears.
- Pair React.memo on a child with useCallback/useMemo on the props the parent passes down.
- Use code splitting to shrink the initial bundle, especially for routes or heavy features not needed immediately.
- Reach for a virtualization library like react-window only once a list is genuinely long.
- Always confirm an optimization actually helped by re-measuring afterward, not just applying it and moving on.
Frequently Asked Questions
Does React re-render the entire app on every state change?
No. React re-renders the component whose state changed and its descendants by default, but sibling components and unrelated parts of the tree are untouched.
Is React.memo the same as useMemo?
No. React.memo wraps a whole component to skip re-rendering when props are unchanged; useMemo caches a value inside a component between renders. They solve related but different problems.
When should I use react-window instead of just rendering a list normally?
When the list is long enough (hundreds or thousands of rows) that rendering every row at once measurably slows the page down. For short lists, plain rendering is simpler and just as fast in practice.
Can too much memoization make an app slower?
Yes, in principle — every memoized value has its own small bookkeeping cost. Used on components that were never slow, it adds complexity without benefit, which is why measuring first matters.
What is the single most important habit for performance work?
Measuring before and after any change. Without real data from a tool like the Profiler, it is easy to "optimize" a component that was never actually a bottleneck.
Key Takeaways
- Components re-render due to their own state, their parent re-rendering, or subscribed context changing.
- React.memo, useMemo, and useCallback work together to skip unnecessary re-renders and recalculations.
- Code splitting shrinks what has to load upfront; virtualization shrinks what has to render for long lists.
- Every optimization tool has a small cost, so apply them based on evidence, not guesswork.
- The React Profiler, covered next, is how you gather that evidence.
Summary
Performance optimization in React is less about memorizing a list of tricks and more about understanding why a re-render happened and whether it was actually expensive. React.memo, useMemo, useCallback, code splitting, and virtualization each address a specific cause, but none of them replace the discipline of measuring first.
In this lesson, you reviewed why components re-render, tied together the optimization tools from earlier lessons, and learned when virtualization is worth the complexity. Next, you will learn to use the React Developer Tools browser extension to actually inspect components and profile renders — the measuring step this lesson has been pointing toward.
- You can explain the common causes of unnecessary re-renders.
- You can describe how React.memo, useMemo, useCallback, and code splitting fit together.
- You know when list virtualization is worth introducing.
- You understand why measuring should come before optimizing.