Skip to main content

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:

  1. Open DevTools → Profiler tab
  2. Click Record → interact → Stop
  3. Flamegraph shows render times per component
  4. 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:

  1. WebSocket updates triggered full dashboard re-render every 2s
  2. No memoization on expensive chart data
  3. Unstable function references broke child memoization
  4. 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-Series03-react