React Performance Patterns That Actually Matter
Practical performance optimization techniques for React applications, focused on real-world scenarios.
Performance optimization in React is often misunderstood. People reach for useMemo and React.memo everywhere, when the real gains come from architectural decisions. Here's what actually matters.
The Biggest Performance Wins
1. Colocate State
The number one cause of unnecessary re-renders is state that lives too high in the component tree:
// ❌ Bad: State in parent causes all children to re-render
function App() {
const [filter, setFilter] = useState('');
return (
<>
<SearchInput value={filter} onChange={setFilter} />
<ExpensiveComponent /> {/* Re-renders on every keystroke! */}
<FilteredList filter={filter} />
</>
);
}
// ✅ Good: Colocate state with the components that use it
function App() {
return (
<>
<SearchSection /> {/* Contains its own state */}
<ExpensiveComponent /> {/* Never re-renders unnecessarily */}
</>
);
}
2. Use Children Pattern
Pass components as children to avoid re-rendering them:
// ❌ Bad: ChildComponent re-renders when parent state changes
function Parent() {
const [count, setCount] = useState(0);
return (
<div onClick={() => setCount(c => c + 1)}>
<ChildComponent />
</div>
);
}
// ✅ Good: Children don't re-render
function Parent({ children }) {
const [count, setCount] = useState(0);
return (
<div onClick={() => setCount(c => c + 1)}>
{children}
</div>
);
}
// Usage
<Parent>
<ChildComponent />
</Parent>
3. Virtualize Long Lists
For lists with many items, virtualization is essential:
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: virtualRow.start,
height: virtualRow.size,
}}
>
{items[virtualRow.index]}
</div>
))}
</div>
</div>
);
}
When to Actually Use memo and useMemo
Use React.memo when:
- The component renders often with the same props
- The component is expensive to render
- You've measured and confirmed it helps
Use useMemo when:
- Creating objects/arrays passed to memoized children
- Expensive calculations that don't need to run every render
// Actually useful useMemo
const sortedItems = useMemo(
() => items.sort((a, b) => a.date - b.date),
[items]
);
// Probably not useful
const doubled = useMemo(() => count * 2, [count]); // Multiplication is cheap
Measure First
Always use React DevTools Profiler before optimizing. You might be surprised where the actual bottlenecks are.
import { Profiler } from 'react';
function onRender(id, phase, actualDuration) {
console.log({ id, phase, actualDuration });
}
<Profiler id="MyComponent" onRender={onRender}>
<MyComponent />
</Profiler>
Conclusion
The best React performance optimizations are architectural:
- Keep state close to where it's used
- Split components at natural boundaries
- Use virtualization for long lists
- Only reach for memo/useMemo when you have measured proof
What performance patterns have you found most effective?