SyntaxStudy
Sign Up
React Profiling and Avoiding Unnecessary Re-renders
React Beginner 1 min read

Profiling and Avoiding Unnecessary Re-renders

The React DevTools Profiler records component renders and highlights which components re-rendered, how long each took, and why the render was triggered. Running a profiling session on a realistic user interaction reveals hotspots — components that re-render far more often than necessary. This is the correct starting point for performance optimisation; never optimise before you measure. Common causes of unnecessary re-renders include passing new object or array literals as props, creating new function instances on every render without useCallback, and having too many components subscribed to a large Context that changes frequently. Isolating frequently-changing state into small, deeply nested components limits the blast radius of each state change. The key insight is that React re-renders are cheap by default; the framework is designed around frequent updates. Optimise only when the Profiler shows a specific render taking longer than the render budget (roughly 16 ms for 60 fps), and always verify the optimisation with a follow-up profiling session.
Example
import React, { useState, memo, useCallback, useMemo } from 'react';

// ── Expensive pure component ───────────────────────────────────────────────
const Row = memo(function Row({ item, onRemove }) {
    console.log(`Rendering row ${item.id}`);
    return (
        <li>
            {item.label}
            <button onClick={() => onRemove(item.id)}>✕</button>
        </li>
    );
});

// ── Parent state is isolated so only changed rows re-render ────────────────
function ItemList({ rawItems, filterText }) {
    const [removedIds, setRemovedIds] = useState(new Set());

    // Stable callback – does not change on re-render
    const handleRemove = useCallback(id => {
        setRemovedIds(prev => new Set([...prev, id]));
    }, []);

    // Expensive filter recomputed only when inputs change
    const visibleItems = useMemo(
        () => rawItems
            .filter(i => !removedIds.has(i.id))
            .filter(i => i.label.toLowerCase().includes(filterText.toLowerCase())),
        [rawItems, removedIds, filterText]
    );

    return (
        <ul>
            {visibleItems.map(item => (
                <Row key={item.id} item={item} onRemove={handleRemove} />
            ))}
        </ul>
    );
}