I've worked on React codebases that felt fast from day one and others that turned into sluggish messes by the time they hit a few hundred components. The difference was rarely the framework. It was the habits.
This isn't a list of micro-optimizations for theoretical bottlenecks. These are the patterns I reach for when something feels slow, in roughly the order I reach for them.
Start with the Profiler, Not a Guess
Before touching a single line of code, open React DevTools and record a Profiler session. Click through whatever feels slow. The flame graph will show you exactly which components are re-rendering and how long each render takes.
Nine times out of ten, the slow part isn't where you thought it was. Optimizing without profiling first is just moving furniture in a dark room.
Unnecessary Re-renders Are the Real Enemy
A component re-renders when its parent re-renders, even if its own props haven't changed. This is fine in small trees. In a large component tree, it compounds fast.
React.memo wraps a component so it only re-renders when its props actually change. Use it on components that are expensive to render and receive the same props frequently.
const ArticleCard = React.memo(function ArticleCard({ title, excerpt }) {
return (
<div>
<h2>{title}</h2>
<p>{excerpt}</p>
</div>
);
});The catch: React.memo does a shallow comparison. If you pass a new object or function reference on every render, it won't help. Which brings us to the next pattern.
Stabilise References with useMemo and useCallback
Every time a component renders, inline objects and functions are recreated. If you pass them as props to a memoized child, the memo check fails every time.
// This creates a new object on every render
const style = { color: 'blue' };
// This only creates a new object when colour changes
const style = useMemo(() => ({ color: colour }), [colour]);
// Same idea for callbacks
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);Don't wrap everything in useMemo by default. It has overhead. Use it when you have a memoized child that's re-rendering unnecessarily, or when you're doing genuinely expensive computation inside a component.
Code Splitting with React.lazy
If your bundle is large, users pay the cost upfront. Code splitting lets you load parts of your app only when they're needed.
const SettingsPage = React.lazy(() => import('./SettingsPage'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<SettingsPage />
</Suspense>
);
}This works best at route boundaries. Split by page, not by component. A settings page that most users never open shouldn't ship in your initial bundle.
Virtualise Long Lists
Rendering a list of 10,000 items means 10,000 DOM nodes. The browser doesn't enjoy that. Virtualisation renders only what's visible on screen.
Libraries like react-window and react-virtual handle this well. If you have a table or list with more than a few hundred rows, virtualisation is not optional.
import { FixedSizeList } from 'react-window';
<FixedSizeList
height={600}
width="100%"
itemCount={items.length}
itemSize={48}
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>Move State Down, Not Up
State that lives at the top of the tree causes the whole tree to re-render when it changes. If only one component cares about a piece of state, put the state there.
This sounds obvious but it's one of the most common performance issues I see. A search input wired to top-level state that re-renders an entire page layout on every keystroke is a classic example. Move the input and its state into a self-contained component.
Context Is Not a Performance Silver Bullet
React Context is great for things that don't change often, like theme or locale. It's a bad fit for frequently changing state because every consumer of the context re-renders when the value changes.
If you're using context for something that updates on user interaction, consider a purpose-built state manager or splitting the context into more granular pieces.
What Not to Optimise
Most components don't need any of this. A component that renders a couple of paragraphs and a button is not your problem. Premature optimisation wastes time and adds complexity.
The pattern I follow: ship it, profile it, fix what the data says is slow. In most apps, 80% of the perceived performance improvement comes from fixing three or four hot spots, not from memoizing everything everywhere.
React is fast by default. The patterns above are for when it stops being fast, not for pre-emptive armour-plating.



