React Rendering Optimization
Difficulty: 🟡 Medium | Frequency: ⭐⭐⭐⭐⭐
The Problem
React re-renders a component whenever its state or props change — and all its descendants. Unnecessary re-renders waste CPU cycles and degrade UX.
Three Optimization Tools
1. React.memo — Memoize Components
Skips re-rendering if props haven't changed (shallow comparison):
const ProductCard = React.memo(function ProductCard({ product, onAddToCart }) {
console.log('Rendering ProductCard:', product.id);
return (
<div>
<h3>{product.name}</h3>
<button onClick={() => onAddToCart(product)}>Add to Cart</button>
</div>
);
});
Custom comparison for complex props:
const Component = React.memo(Component, (prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id; // true = skip render
});
2. useMemo — Memoize Values
const filteredProducts = useMemo(() =>
products.filter(p => p.category === category && p.price <= maxPrice),
[products, category, maxPrice]
);
3. useCallback — Memoize Functions
const handleAddToCart = useCallback((product) => {
setCart(prev => [...prev, product]);
}, []); // Stable reference → ProductCard.memo works
They Work Together
function ProductList({ products }) {
const [cart, setCart] = useState([]);
const [filter, setFilter] = useState('');
// Stable function reference
const handleAddToCart = useCallback((product) => {
setCart(prev => [...prev, product]);
}, []);
// Expensive filter only runs when dependencies change
const filtered = useMemo(() =>
products.filter(p => p.name.includes(filter)),
[products, filter]
);
return (
<>
<input value={filter} onChange={e => setFilter(e.target.value)} />
{filtered.map(p => (
// Only re-renders if product or onAddToCart changes
<ProductCard key={p.id} product={p} onAddToCart={handleAddToCart} />
))}
</>
);
}
Profiling: Find Real Bottlenecks First
React DevTools Profiler:
- Open DevTools → Profiler tab
- Click Record → interact → Stop
- Flamegraph shows render times per component
- Gray = skipped (memoized), yellow = fast, orange/red = slow
import { Profiler } from 'react';
function onRender(id, phase, actualDuration, baseDuration) {
if (actualDuration > 16) {
console.warn(`Slow render: ${id} took ${actualDuration}ms`);
}
}
<Profiler id="App" onRender={onRender}>
<Dashboard />
</Profiler>
baseDuration vs actualDuration: The gap shows memoization benefit. Large gap = memoization is helping.
Common Anti-Patterns That Break Memoization
// ❌ New object every render — React.memo always re-renders
function Parent() {
return <Child style={{ color: 'red' }} />; // New object!
}
// ✅ Stable reference
const style = { color: 'red' };
function Parent() {
return <Child style={style} />;
}
// ❌ New function every render
function Parent() {
return <Child onClick={() => handleClick(id)} />; // New function!
}
// ✅ Stable callback
const handleClick = useCallback(() => {/* ... */}, [id]);
When NOT to Memoize
Memoization adds overhead:
- Comparison function runs every render
- Values are stored in memory
Skip memoization when:
- Component renders in <5ms
- Props change every render anyway
- You haven't profiled and confirmed a bottleneck
Virtualization for Long Lists
react-window or react-virtual renders only visible items:
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
);
}
// 10,000 items → only ~12 DOM nodes rendered
Real-World: Dashboard Crisis
Before optimization: 2,847ms per filter interaction, 247 component renders, 8fps.
Root causes:
- WebSocket updates triggered full dashboard re-render every 2s
- No memoization on expensive chart data
- Unstable function references broke child memoization
- 10,000 rows rendered without virtualization
After optimization (targeted useMemo + useCallback + react-window):
- Filter response: 2,847ms → 187ms (93% faster)
- FPS: 8 → 58–60
- Renders per interaction: 247 → 8
Content from Frontend-Master-Prep-Series — 03-react