Introduction
React's component model is intuitive, but it can lead to subtle performance problems as your app grows — excessive re-renders, large bundle sizes, and memory leaks are the most common culprits. This guide covers the patterns that actually move the needle.
Memoization: useMemo & useCallback
Both hooks prevent re-computation on every render. The key rule: don't use them indiscriminately. The memo itself has a cost. Only apply when:
•The computation is genuinely expensive •You need referential equality (callbacks passed to child components, dependencies in other hooks)
"hl-keyword">import { useMemo, useCallback, memo } "hl-keyword">from 'react';
"hl-keyword">class="hl-comment">// ❌ Pointless — addition is cheaper than the memo
"hl-keyword">const bad = useMemo(() => a + b, [a, b]);
"hl-keyword">class="hl-comment">// ✅ Expensive filter on a large list
"hl-keyword">const filteredItems = useMemo(
() => items.filter(item => item.category === activeCategory),
[items, activeCategory] "hl-keyword">class="hl-comment">// only re-runs when these change
);
"hl-keyword">class="hl-comment">// ✅ Stable callback reference — prevents child re-renders
"hl-keyword">const handleDelete = useCallback(
(id: "hl-type">string) => {
dispatch({ "hl-keyword">type: 'DELETE_ITEM', payload: id });
},
[dispatch] "hl-keyword">class="hl-comment">// dispatch "hl-keyword">from useReducer is already stable
);
"hl-keyword">class="hl-comment">// Wrap child with memo so it only re-renders when props change
"hl-keyword">const ItemRow = memo(({ item, onDelete }: Props) => (
<li>
{item.name}
<button onClick={() => onDelete(item.id)}>Delete</button>
</li>
));
Code Splitting & Lazy Loading
React.lazy + Suspense loads components only when they're needed. This can cut your initial bundle size by 40–70% for complex apps with multiple routes.
"hl-keyword">import { lazy, Suspense } "hl-keyword">from 'react';
"hl-keyword">import { Routes, Route } "hl-keyword">from 'react-router-dom';
"hl-keyword">class="hl-comment">// These components are loaded ONLY when the route is visited
"hl-keyword">const Dashboard = lazy(() => "hl-keyword">import('./pages/Dashboard'));
"hl-keyword">const Settings = lazy(() => "hl-keyword">import('./pages/Settings'));
"hl-keyword">const Editor = lazy(() => "hl-keyword">import('./pages/Editor'));
"hl-keyword">function App() {
"hl-keyword">return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/editor" element={<Editor />} />
</Routes>
</Suspense>
);
}
"hl-keyword">class="hl-comment">// Prefetch on hover — load the chunk before the user clicks
"hl-keyword">function NavLink({ to, children }: Props) {
"hl-keyword">const preload = () => "hl-keyword">import(`./pages/${to}`);
"hl-keyword">return (
<a href={to} onMouseEnter={preload}>
{children}
</a>
);
}
Virtualisation for Long Lists
Never render thousands of DOM nodes. Use virtualisation (also called windowing) to render only the visible rows. @tanstack/virtual is the modern choice.
"hl-keyword">import { useVirtualizer } "hl-keyword">from '@tanstack/react-virtual';
"hl-keyword">import { useRef } "hl-keyword">from 'react';
"hl-keyword">function VirtualList({ items }: { items: "hl-type">string[] }) {
"hl-keyword">const parentRef = useRef<HTMLDivElement>("hl-type">null);
"hl-keyword">const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 56, "hl-keyword">class="hl-comment">// estimated row height "hl-keyword">in px
overscan: 5, "hl-keyword">class="hl-comment">// extra rows rendered outside viewport
});
"hl-keyword">return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
{/* Container with the full list height so the scrollbar is correct */}
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.index}
style={{
position: 'absolute',
top: virtualRow.start,
height: virtualRow.size,
width: '100%',
}}
>
{items[virtualRow.index]}
</div>
))}
</div>
</div>
);
}