React Performance Optimization
Understanding and optimizing React performance is crucial for building fast applications.
How React Renders
State/Props Change → Component Re-renders → Virtual DOM Diff → DOM Updates
React re-renders a component when:
- Its state changes
- Its props change
- Its parent re-renders
- Context it consumes changes
React.memo
Prevent re-renders when props haven't changed:
jsx
1// Without memo - re-renders on every parent render
2function ExpensiveList({ items }) {
3 console.log('Rendering list...')
4 return items.map(item => <Item key={item.id} item={item} />)
5}
6
7// With memo - only re-renders when items change
8const ExpensiveList = React.memo(function ExpensiveList({ items }) {
9 console.log('Rendering list...')
10 return items.map(item => <Item key={item.id} item={item} />)
11})Custom Comparison
jsx
1const MemoizedComponent = React.memo(
2 function Component({ user, onClick }) {
3 return <div onClick={onClick}>{user.name}</div>
4 },
5 (prevProps, nextProps) => {
6 // Return true if props are equal (skip re-render)
7 return prevProps.user.id === nextProps.user.id
8 }
9)useMemo for Expensive Calculations
jsx
1function ProductList({ products, filter }) {
2 // ❌ Runs on every render
3 const filteredProducts = products.filter(p => p.category === filter)
4
5 // ✅ Only recalculates when dependencies change
6 const filteredProducts = useMemo(() => {
7 console.log('Filtering products...')
8 return products.filter(p => p.category === filter)
9 }, [products, filter])
10
11 return (
12 <ul>
13 {filteredProducts.map(p => <ProductItem key={p.id} product={p} />)}
14 </ul>
15 )
16}useCallback for Stable Function References
jsx
1function Parent() {
2 const [count, setCount] = useState(0)
3
4 // ❌ New function on every render
5 const handleClick = () => console.log('clicked')
6
7 // ✅ Stable reference
8 const handleClick = useCallback(() => {
9 console.log('clicked')
10 }, [])
11
12 return <MemoizedChild onClick={handleClick} />
13}
14
15// Child only re-renders if onClick reference changes
16const MemoizedChild = React.memo(function Child({ onClick }) {
17 console.log('Child rendered')
18 return <button onClick={onClick}>Click</button>
19})Avoiding Unnecessary Re-renders
Move State Down
jsx
1// ❌ Bad - entire app re-renders on input change
2function App() {
3 const [text, setText] = useState('')
4
5 return (
6 <div>
7 <Header />
8 <input value={text} onChange={e => setText(e.target.value)} />
9 <ExpensiveComponent />
10 <Footer />
11 </div>
12 )
13}
14
15// ✅ Good - only SearchBox re-renders
16function App() {
17 return (
18 <div>
19 <Header />
20 <SearchBox />
21 <ExpensiveComponent />
22 <Footer />
23 </div>
24 )
25}
26
27function SearchBox() {
28 const [text, setText] = useState('')
29 return <input value={text} onChange={e => setText(e.target.value)} />
30}Lift Content Up
jsx
1// ❌ Bad - children re-render on color change
2function ColorPicker() {
3 const [color, setColor] = useState('red')
4
5 return (
6 <div style={{ backgroundColor: color }}>
7 <input value={color} onChange={e => setColor(e.target.value)} />
8 <ExpensiveTree /> {/* Re-renders every time! */}
9 </div>
10 )
11}
12
13// ✅ Good - children passed as prop don't re-render
14function App() {
15 return (
16 <ColorPicker>
17 <ExpensiveTree />
18 </ColorPicker>
19 )
20}
21
22function ColorPicker({ children }) {
23 const [color, setColor] = useState('red')
24
25 return (
26 <div style={{ backgroundColor: color }}>
27 <input value={color} onChange={e => setColor(e.target.value)} />
28 {children} {/* Doesn't re-render! */}
29 </div>
30 )
31}Virtualization for Long Lists
Only render visible items:
bash
1npm install @tanstack/react-virtualjsx
1import { useVirtualizer } from '@tanstack/react-virtual'
2
3function VirtualList({ items }) {
4 const parentRef = useRef(null)
5
6 const virtualizer = useVirtualizer({
7 count: items.length,
8 getScrollElement: () => parentRef.current,
9 estimateSize: () => 50,
10 })
11
12 return (
13 <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
14 <div
15 style={{
16 height: `${virtualizer.getTotalSize()}px`,
17 position: 'relative',
18 }}
19 >
20 {virtualizer.getVirtualItems().map(virtualItem => (
21 <div
22 key={virtualItem.key}
23 style={{
24 position: 'absolute',
25 top: 0,
26 left: 0,
27 width: '100%',
28 height: `${virtualItem.size}px`,
29 transform: `translateY(${virtualItem.start}px)`,
30 }}
31 >
32 {items[virtualItem.index].name}
33 </div>
34 ))}
35 </div>
36 </div>
37 )
38}Debouncing User Input
jsx
1function SearchInput() {
2 const [query, setQuery] = useState('')
3 const [debouncedQuery, setDebouncedQuery] = useState('')
4
5 useEffect(() => {
6 const timer = setTimeout(() => {
7 setDebouncedQuery(query)
8 }, 300)
9
10 return () => clearTimeout(timer)
11 }, [query])
12
13 useEffect(() => {
14 if (debouncedQuery) {
15 performSearch(debouncedQuery)
16 }
17 }, [debouncedQuery])
18
19 return (
20 <input
21 value={query}
22 onChange={e => setQuery(e.target.value)}
23 placeholder="Search..."
24 />
25 )
26}Profiling
Use React DevTools Profiler:
- Open React DevTools
- Go to "Profiler" tab
- Click "Record"
- Interact with your app
- Click "Stop"
- Analyze render times and causes
Key Performance Tips
- Don't optimize prematurely - Measure first
- Use React.memo strategically - Not everywhere
- Keep state local - Only lift when needed
- Virtualize long lists - 1000+ items
- Lazy load components - Code splitting
- Use Web Workers - Heavy computations
Performance optimization is about making informed decisions!
