React Performance Optimization
React is fast by default, but complex applications suffer from unnecessary re-renders and large bundle sizes. This guide covers systematic performance optimization.
useMemo and useCallback
function ProductList({ products, onSelect }: Props) {
const [filter, setFilter] = useState('');
// Only recomputes when dependencies change
const filtered = useMemo(() => {
return products
.filter(p => p.name.toLowerCase().includes(filter.toLowerCase()))
.sort((a, b) => a.name > b.name ? 1 : -1);
}, [products, filter]);
// Stable reference for memoized children
const handleSelect = useCallback((id: string) => {
onSelect(id);
}, [onSelect]);
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
{filtered.map(product => (
<ProductCard
key={product.id}
product={product}
onSelect={handleSelect}
/>
))}
</div>
);
}
const ProductCard = React.memo(({ product, onSelect }: Props) => {
return <div onClick={() => onSelect(product.id)}>{product.name}</div>;
});
Virtual Lists for Large Data
import { FixedSizeList } from 'react-window';
const Row = ({ index, style, data }: any) => {
const item = data.items[index];
return (
<div style={style} onClick={() => data.onSelect(item.id)}>
{item.name} - ${item.price}
</div>
);
};
function VirtualProductList({ products, onSelect }: Props) {
const itemData = useMemo(
() => ({ items: products, onSelect }),
[products, onSelect]
);
return (
<FixedSizeList
height={600}
itemCount={products.length}
itemSize={60}
itemData={itemData}
>
{Row}
</FixedSizeList>
);
}
Code Splitting and Lazy Loading
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
);
}
useTransition and useDeferredValue
function SearchPage() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setQuery(value); // Urgent: update input immediately
startTransition(() => {
// Non-urgent: can be interrupted if user types again
setSearchResults(performSearch(value));
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{isPending ? <Spinner /> : <Results />}
</div>
);
}
// useDeferredValue for expensive renders
function SearchResults({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const results = useMemo(
() => performExpensiveSearch(deferredQuery),
[deferredQuery]
);
return (
<div style={{ opacity: isStale ? 0.7 : 1 }}>
{results.map(r => <ResultItem key={r.id} result={r} />)}
</div>
);
}
Context Optimization
// BAD: Single context causes all consumers to re-render
const AppContext = createContext({ user: null, cart: [], theme: 'light' });
// GOOD: Split contexts by update frequency
const UserContext = createContext<User | null>(null);
const CartContext = createContext<Cart>({ items: [], total: 0 });
const ThemeContext = createContext<'light' | 'dark'>('light');
Performance Checklist
- Profile with React DevTools Profiler first
- Add
React.memoto components with stable props - Use
useMemofor expensive computations - Use
useCallbackfor handlers passed to memoized children - Virtualize lists with 100+ items
- Code split routes and heavy components
- Use
useTransitionfor non-urgent updates - Avoid inline objects/arrays in JSX props
Summary
Profile first, then optimize. Over-memoization adds complexity without benefit. React's built-in tools (useTransition, useDeferredValue) handle concurrent rendering gracefully.