Performance Optimization
Memoization, code splitting, and React best practices
React Performance Basics
React is fast by default, but as your app grows, you might notice performance issues. Understanding how React renders and when to optimize is key to building fast applications.
The Golden Rule
Don't optimize prematurely. Measure first, optimize only when you have actual performance problems. Most React apps are fast enough without optimization.
Understanding Re-renders
React re-renders a component when:
// 1. State changes
function Counter() {
const [count, setCount] = useState(0);
// Every setCount call triggers a re-render
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// 2. Props change
function Child({ name }) {
// Re-renders when 'name' prop changes
return <p>{name}</p>;
}
// 3. Parent re-renders (even if props don't change!)
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<Child name="John" /> {/* Re-renders even though name never changes! */}
</div>
);
}
// 4. Context changes
const ThemeContext = createContext();
function ThemedButton() {
const theme = useContext(ThemeContext);
// Re-renders when context value changes
return <button className={theme}>Click</button>;
}
React.memo
Prevents re-renders when props haven't changed:
import { memo } from 'react';
// Without memo: re-renders whenever parent re-renders
function ExpensiveComponent({ data }) {
// Expensive rendering logic...
return <div>{data.name}</div>;
}
// With memo: only re-renders when props change
const MemoizedComponent = memo(function ExpensiveComponent({ data }) {
return <div>{data.name}</div>;
});
// Custom comparison function
const MemoizedWithCompare = memo(
function Component({ user }) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return prevProps.user.id === nextProps.user.id;
}
);
⚠️ memo Pitfall
memo only works if props are referentially stable. New objects/arrays/functions break memoization!
useMemo - Memoize Values
import { useMemo } from 'react';
function FilteredList({ items, filter }) {
// Without useMemo: recalculates on every render
// const filtered = items.filter(i => i.name.includes(filter));
// With useMemo: only recalculates when items or filter change
const filtered = useMemo(() => {
console.log('Filtering...');
return items.filter(i => i.name.includes(filter));
}, [items, filter]);
return (
<ul>
{filtered.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}
// Expensive calculation
function Fibonacci({ n }) {
const result = useMemo(() => {
const fib = (num) => num <= 1 ? num : fib(num - 1) + fib(num - 2);
return fib(n);
}, [n]);
return <p>Result: {result}</p>;
}
useCallback - Memoize Functions
import { useCallback, memo } from 'react';
// Child wrapped in memo
const Button = memo(function Button({ onClick, children }) {
console.log('Button rendered');
return <button onClick={onClick}>{children}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// Without useCallback: new function every render, breaks memo
// const handleClick = () => console.log('clicked');
// With useCallback: same function reference
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
// With dependencies
const handleSubmit = useCallback(() => {
console.log('Submitting count:', count);
}, [count]); // New function only when count changes
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<Button onClick={handleClick}>Click me</Button>
</div>
);
}
Code Splitting with lazy()
import { lazy, Suspense } from 'react';
// Lazy load components
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// Named exports
const Modal = lazy(() =>
import('./components').then(module => ({ default: module.Modal }))
);
Virtualization for Long Lists
// npm install @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // Estimated row height
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: virtualRow.size,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
);
}
// Only renders visible items, not all 10,000!
Debouncing and Throttling
import { useState, useMemo } from 'react';
import debounce from 'lodash/debounce';
function SearchInput() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
// Debounced search function
const debouncedSearch = useMemo(
() => debounce(async (searchQuery) => {
const data = await fetch(`/api/search?q=${searchQuery}`);
setResults(await data.json());
}, 300),
[]
);
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value);
};
return (
<div>
<input value={query} onChange={handleChange} />
<ul>
{results.map(r => <li key={r.id}>{r.name}</li>)}
</ul>
</div>
);
}
Measuring Performance
// React DevTools Profiler
// 1. Open React DevTools
// 2. Go to Profiler tab
// 3. Click Record, interact with app, stop
// 4. Analyze which components render and why
// Console timing
function ExpensiveComponent() {
console.time('render');
// ... component logic
console.timeEnd('render');
return <div>...</div>;
}
// React.Profiler component
import { Profiler } from 'react';
function onRenderCallback(
id, // Component id
phase, // "mount" or "update"
actualDuration, // Time spent rendering
baseDuration, // Estimated time without memoization
) {
console.log(`${id} ${phase}: ${actualDuration}ms`);
}
<Profiler id="Navigation" onRender={onRenderCallback}>
<Navigation />
</Profiler>
Common Performance Issues
// ❌ Creating objects/arrays in render
function Bad() {
return <Child style={{ color: 'red' }} />; // New object every render!
}
// ✅ Move outside or memoize
const style = { color: 'red' };
function Good() {
return <Child style={style} />;
}
// ❌ Inline function props
function Bad() {
return <Button onClick={() => doSomething()} />; // New function every render!
}
// ✅ Use useCallback
function Good() {
const handleClick = useCallback(() => doSomething(), []);
return <Button onClick={handleClick} />;
}
// ❌ State too high in tree
function App() {
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
// Every mouse move re-renders entire app!
}
// ✅ Keep state close to where it's used
function MouseTracker() {
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
// Only this component re-renders
}
🎯 Performance Best Practices
- ✓ Measure before optimizing—use React DevTools Profiler
- ✓ Keep state as local as possible
- ✓ Use React.memo for expensive pure components
- ✓ Use useMemo for expensive calculations
- ✓ Use useCallback for stable function references
- ✓ Lazy load routes and heavy components
- ✓ Virtualize long lists (1000+ items)
- ✓ Debounce/throttle frequent events
- ✓ Avoid unnecessary re-renders from context