Component Optimization
Master React performance optimization techniques. Learn how to identify and fix performance bottlenecks, prevent unnecessary re-renders, and create fast, responsive UIs.
Quick Reference
Prevent Re-renders with React.memo
const MemoizedComponent = React.memo(function Component({ data }) {
return <div>{data}</div>;
});
Memoize Expensive Calculations
const result = useMemo(() => expensiveCalculation(data), [data] );
Memoize Callback Functions
const handleClick = useCallback(() => {
doSomething(value);
}, [value]);
Code Splitting
const LazyComponent = lazy(() => import('./Component'));
<Suspense fallback={<Spinner />}>
<LazyComponent />
</Suspense>
When to Use This Skill
- •Component renders slowly or causes UI lag
- •Large lists or tables perform poorly
- •Callbacks cause child components to re-render
- •Initial page load is too slow
- •Bundle size is too large
- •Profiler shows unnecessary re-renders
Understanding Re-renders
When Does React Re-render?
React re-renders a component when:
- •State changes -
useStateoruseReducerupdates - •Props change - Parent passes new props
- •Parent re-renders - By default, all children re-render
- •Context changes - Any context value updates
The Re-render Problem
// ❌ PROBLEM: Child re-renders unnecessarily
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<ExpensiveChild /> {/* Re-renders every time! */}
</div>
);
}
Even though ExpensiveChild doesn't use count, it re-renders whenever the parent does.
Optimization Techniques
1. React.memo - Prevent Unnecessary Re-renders
React.memo skips re-rendering if props haven't changed.
// ✅ SOLUTION: Memoize the child
const ExpensiveChild = React.memo(function ExpensiveChild() {
console.log("ExpensiveChild rendered");
return (
<div>
{/* Expensive rendering logic */}
</div>
);
});
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<ExpensiveChild /> {/* No longer re-renders! */}
</div>
);
}
When to use:
- •Component renders often but props rarely change
- •Component has expensive rendering logic
- •Component is large with many children
When NOT to use:
- •Props change frequently
- •Component is simple and fast
- •Premature optimization
2. useMemo - Memoize Expensive Calculations
useMemo caches calculation results between renders.
function ProductList({ products, filters }) {
// ❌ Recalculates on every render
const filtered = products.filter(p =>
p.category === filters.category
);
// ✅ Only recalculates when dependencies change
const filtered = useMemo(
() => products.filter(p => p.category === filters.category),
[products, filters.category]
);
return (
<ul>
{filtered.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
When to use:
- •Expensive calculations (filtering, sorting, mapping large arrays)
- •Derived state from props or state
- •Creating objects/arrays passed as props to memoized children
When NOT to use:
- •Simple calculations (they're fast enough)
- •Values that change every render anyway
- •Premature optimization
3. useCallback - Memoize Functions
useCallback prevents creating new function instances on every render.
function TodoList({ todos }) {
const [filter, setFilter] = useState("all");
// ❌ New function every render, breaks React.memo
const handleComplete = (id) => {
completeTodo(id);
};
// ✅ Same function reference between renders
const handleComplete = useCallback((id) => {
completeTodo(id);
}, []);
return (
<ul>
{todos.map(todo => (
<MemoizedTodoItem
key={todo.id}
todo={todo}
onComplete={handleComplete}
/>
))}
</ul>
);
}
When to use:
- •Passing callbacks to memoized children
- •Functions used in dependency arrays
- •Functions passed to many children
When NOT to use:
- •Functions not passed as props
- •Parent component re-renders frequently anyway
- •Simple event handlers
4. Code Splitting with React.lazy
Split large components into separate bundles that load on demand.
import { lazy, Suspense } from "react";
// ✅ Only loads when needed
const AdminPanel = lazy(() => import("./AdminPanel"));
const Charts = lazy(() => import("./Charts"));
function Dashboard() {
const [showAdmin, setShowAdmin] = useState(false);
return (
<div>
<h1>Dashboard</h1>
{showAdmin && (
<Suspense fallback={<Spinner />}>
<AdminPanel />
</Suspense>
)}
<Suspense fallback={<ChartsSkeleton />}>
<Charts />
</Suspense>
</div>
);
}
When to use:
- •Large components not needed initially
- •Admin/authenticated sections
- •Modals and dialogs
- •Route-based code splitting
5. Virtualization for Long Lists
Render only visible items in long lists.
import { useVirtualizer } from "@tanstack/react-virtual";
function VirtualList({ items }) {
const parentRef = useRef<HTMLDivElement>(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()}px` }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
);
}
When to use:
- •Lists with 100+ items
- •Tables with many rows
- •Infinite scrolling
- •Chat messages or feeds
Advanced Patterns
1. Context Optimization
Split contexts to prevent unnecessary re-renders:
// ❌ Everything re-renders when anything changes
const AppContext = createContext({
user: null,
theme: "light",
settings: {},
});
// ✅ Separate contexts for independent data
const UserContext = createContext(null);
const ThemeContext = createContext("light");
const SettingsContext = createContext({});
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("light");
const [settings, setSettings] = useState({});
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<SettingsContext.Provider value={settings}>
<AppContent />
</SettingsContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}
2. Component Composition vs Props
Use children instead of passing components as props:
// ❌ Slower prop creates new component
function Parent({ content }) {
const [state, setState] = useState(0);
return <div>{content}</div>;
}
<Parent content={<ExpensiveComponent />} />
// ✅ Faster - children don't re-render
function Parent({ children }) {
const [state, setState] = useState(0);
return <div>{children}</div>;
}
<Parent>
<ExpensiveComponent />
</Parent>
3. State Colocation
Move state closer to where it's used:
// ❌ Top-level state causes full tree re-render
function App() {
const [color, setColor] = useState("blue");
return (
<div>
<ColorPicker color={color} onChange={setColor} />
<ExpensiveComponent />
<AnotherExpensiveComponent />
</div>
);
}
// ✅ Isolated state only affects relevant components
function App() {
return (
<div>
<ColorPickerSection />
<ExpensiveComponent />
<AnotherExpensiveComponent />
</div>
);
}
function ColorPickerSection() {
const [color, setColor] = useState("blue");
return <ColorPicker color={color} onChange={setColor} />;
}
Profiling Performance
1. React DevTools Profiler
// Add Profiler to measure render times
import { Profiler } from "react";
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<AppContent />
</Profiler>
);
}
function onRenderCallback(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) {
console.log(`${id} (${phase}) took ${actualDuration}ms`);
}
2. Performance Timeline
// Mark performance in browser DevTools
function expensiveOperation() {
performance.mark("expensive-start");
// ... expensive work ...
performance.mark("expensive-end");
performance.measure(
"expensive-operation",
"expensive-start",
"expensive-end"
);
}
Common Issues
Issue 1: React.memo Not Working
Symptoms: Memoized component still re-renders Cause: Props include objects/arrays/functions that change reference Solution: Memoize prop values
// ❌ New object every render
<MemoizedChild config={{ theme: "dark" }} />
// ✅ Stable reference
const config = useMemo(() => ({ theme: "dark" }), []);
<MemoizedChild config={config} />
// ✅ Or use individual props
<MemoizedChild theme="dark" />
Issue 2: Over-Optimization
Symptoms: Code is complex but not faster Cause: Memoization overhead exceeds benefit Solution: Remove unnecessary optimization
// ❌ Overkill - simple operations are fast const result = useMemo(() => a + b, [a, b]); // ✅ Just calculate it const result = a + b;
Issue 3: Stale Closures
Symptoms: useCallback uses old values Cause: Missing dependencies Solution: Add all dependencies
// ❌ Uses stale 'count'
const handleClick = useCallback(() => {
console.log(count);
}, []);
// ✅ Always current
const handleClick = useCallback(() => {
console.log(count);
}, [count]);
Best Practices
- • Profile before optimizing (use React DevTools)
- • Start with proper state colocation
- • Use React.memo for expensive components with stable props
- • Use useMemo for expensive calculations, not simple ones
- • Use useCallback for functions passed to memoized children
- • Split large components into smaller ones
- • Use code splitting for large, conditionally rendered components
- • Virtualize long lists (100+ items)
- • Split contexts to reduce re-render scope
- • Measure impact - ensure optimization actually helps
Anti-Patterns
Things to avoid:
- •❌ Memoizing everything by default
- •❌ Premature optimization without measuring
- •❌ Using useMemo for simple calculations
- •❌ Forgetting dependencies in hooks
- •❌ Creating new objects/arrays in render
- •❌ Deeply nested component trees
- •❌ Massive components with too many responsibilities
- •❌ Global state for local concerns
Performance Checklist
When debugging slow components:
- •Identify - Use Profiler to find slow components
- •Measure - Record baseline performance
- •Analyze - Check why component renders
- •Optimize - Apply appropriate technique
- •Verify - Measure improvement
- •Document - Note why optimization was needed