10 Advanced Techniques for Optimizing React Performance
React is powerful, but without careful optimization, applications can become sluggish as they grow. Here are 10 advanced techniques I've used to keep React apps fast and responsive.
1. Memoize Expensive Computations with useMemo
When you have computations that are expensive and don't need to re-run on every render, useMemo is your best friend.
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.price - b.price);
}, [items]);
Only recalculates when items actually changes — not on every parent re-render.
2. Prevent Unnecessary Re-renders with React.memo
Wrap components that receive the same props frequently. React.memo does a shallow comparison and skips re-rendering if props haven't changed.
const ProductCard = React.memo(({ product, onAdd }) => {
return (
<div>
<h3>{product.name}</h3>
<button onClick={() => onAdd(product.id)}>Add</button>
</div>
);
});
Important: This only works if you also stabilize callback references with useCallback.
3. Stabilize Callbacks with useCallback
Without useCallback, every render creates a new function reference — breaking React.memo on child components.
const handleAdd = useCallback((id) => {
setCart(prev => [...prev, id]);
}, []);
4. Virtualize Long Lists
Rendering 10,000 DOM nodes kills performance. Libraries like react-window or @tanstack/virtual only render what's visible in the viewport.
import { FixedSizeList } from 'react-window';
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
This takes you from rendering thousands of elements to rendering ~20 at any given time.
5. Code Split with React.lazy and Suspense
Don't load everything upfront. Split your bundle so users only download what they need.
const Dashboard = React.lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>
);
}
Combined with route-based splitting, this can dramatically reduce initial load time.
6. Debounce Expensive Operations
Search inputs, window resize handlers, scroll listeners — these fire rapidly. Debounce them to avoid hammering your app with state updates.
const debouncedSearch = useMemo(
() => debounce((query) => setSearchTerm(query), 300),
[]
);
7. Use Keys Correctly in Lists
React uses keys to track which items changed. Using array indices as keys causes unnecessary re-renders when items are reordered or removed.
// Bad - index keys cause issues on reorder
{items.map((item, i) => <Item key={i} {...item} />)}
// Good - stable unique keys
{items.map(item => <Item key={item.id} {...item} />)}
8. Avoid Inline Object/Array Creation in JSX
Every render creates a new reference, which means child components always see "new" props.
// Bad - new object every render
<Map center={{ lat: 0, lng: 0 }} />
// Good - stable reference
const center = useMemo(() => ({ lat: 0, lng: 0 }), []);
<Map center={center} />
9. Profile Before Optimizing
Don't guess where the bottleneck is. Use React DevTools Profiler to measure actual render times and identify which components re-render unnecessarily.
Steps:
- Open React DevTools → Profiler tab
- Click Record
- Interact with your app
- Stop recording and analyze the flamegraph
Look for components that render frequently with the same props — those are candidates for React.memo.
10. Use Transitions for Non-Urgent Updates
React 18's useTransition lets you mark state updates as non-urgent, keeping the UI responsive during heavy computations.
const [isPending, startTransition] = useTransition();
function handleFilter(value) {
// Urgent: update input immediately
setInputValue(value);
// Non-urgent: filter results can wait
startTransition(() => {
setFilteredResults(filterItems(value));
});
}
Summary
| Technique | Best For |
|---|---|
| useMemo | Expensive calculations |
| React.memo | Components with stable props |
| useCallback | Stabilizing function references |
| Virtualization | Long lists (100+ items) |
| Code splitting | Reducing initial bundle size |
| Debouncing | Rapid-fire events (search, scroll) |
| Proper keys | Dynamic lists |
| Stable references | Preventing unnecessary re-renders |
| Profiling | Finding actual bottlenecks |
| useTransition | Heavy non-urgent updates |
The most important lesson: measure first, optimize second. React is already fast for most use cases. Only add complexity when profiling shows a real problem.
Have questions about React performance? Connect with me on Twitter or LinkedIn!