React Performance Patterns I Use Daily
After working on several large React applications, I've developed a toolkit of performance patterns that I reach for regularly. Here are the ones that have made the biggest impact.
1. Memoization Done Right
The key is knowing when to memoize, not just how:
// ❌ Unnecessary memoization
const SimpleButton = memo(({ onClick, label }) => (
<button onClick={onClick}>{label}</button>
))
// ✅ Beneficial memoization - expensive render
const ExpensiveList = memo(({ items }) => (
<ul>
{items.map(item => (
<ComplexListItem key={item.id} {...item} />
))}
</ul>
))
2. Stable References with useCallback
function SearchComponent() {
const [query, setQuery] = useState('')
// ✅ Stable reference prevents child re-renders
const handleSearch = useCallback((value: string) => {
setQuery(value)
}, [])
return <SearchInput onSearch={handleSearch} />
}
3. State Colocation
Keep state as close to where it's used as possible:
// ❌ State too high in the tree
function App() {
const [isTooltipOpen, setIsTooltipOpen] = useState(false)
return (
<Layout>
<Header />
<Content>
<Button tooltip={isTooltipOpen} setTooltip={setIsTooltipOpen} />
</Content>
</Layout>
)
}
// ✅ State colocated with usage
function Button() {
const [isTooltipOpen, setIsTooltipOpen] = useState(false)
return (
<button onMouseEnter={() => setIsTooltipOpen(true)}>
{isTooltipOpen && <Tooltip />}
</button>
)
}
4. Virtualization for Long Lists
For lists with hundreds of 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].name}
</div>
))}
</div>
</div>
)
}
5. Lazy Loading Components
Split your bundle and load components on demand:
import { lazy, Suspense } from 'react'
const HeavyChart = lazy(() => import('./HeavyChart'))
function Dashboard() {
return (
<div>
<Header />
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
</div>
)
}
Measuring Performance
Always measure before optimizing. Use React DevTools Profiler and these custom hooks:
function useRenderCount(componentName: string) {
const renderCount = useRef(0)
useEffect(() => {
renderCount.current += 1
console.log(`${componentName} rendered ${renderCount.current} times`)
})
}
Key Takeaways
- Don't optimize prematurely - measure first
- Memoization has a cost - use it wisely
- State colocation is often the best optimization
- Virtualize long lists
- Code-split at route boundaries
Performance optimization is an ongoing process. Start with the patterns that give you the biggest wins for your specific use case.