Performance is crucial for user experience and SEO. A slow React application can lead to higher bounce rates and lower conversion. This guide covers advanced techniques to optimize your React applications for maximum performance.
Understanding React's Rendering Behavior
Before optimizing, understand how React decides to re-render components:
// Every state change triggers a re-render of this component
// AND all its children
function ParentComponent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<ExpensiveChild /> {/* Re-renders even though it doesn't use count! */}
</div>
);
}
React re-renders all child components when a parent re-renders, regardless of whether their props changed. This is the primary source of performance issues.
Memoization Techniques
React.memo for Component Memoization
Prevent unnecessary re-renders with React.memo:
import { memo } from 'react';
// Without memo - re-renders on every parent render
function ProductCard({ product, onAddToCart }) {
console.log('ProductCard rendered');
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => onAddToCart(product.id)}>
Add to Cart
</button>
</div>
);
}
// With memo - only re-renders when props change
const MemoizedProductCard = memo(ProductCard);
// Custom comparison function for complex props
const MemoizedProductCardCustom = memo(ProductCard, (prevProps, nextProps) => {
return prevProps.product.id === nextProps.product.id &&
prevProps.product.price === nextProps.product.price;
});
useMemo for Expensive Calculations
import { useMemo } from 'react';
function ProductList({ products, filters }) {
// Without useMemo - recalculates on every render
// const filteredProducts = products.filter(p =>
// p.category === filters.category && p.price <= filters.maxPrice
// );
// With useMemo - only recalculates when dependencies change
const filteredProducts = useMemo(() => {
console.log('Filtering products...');
return products.filter(p =>
p.category === filters.category &&
p.price <= filters.maxPrice
);
}, [products, filters.category, filters.maxPrice]);
// Expensive sorting operation
const sortedProducts = useMemo(() => {
return [...filteredProducts].sort((a, b) => {
if (filters.sortBy === 'price') return a.price - b.price;
if (filters.sortBy === 'name') return a.name.localeCompare(b.name);
return 0;
});
}, [filteredProducts, filters.sortBy]);
return (
<div className="product-grid">
{sortedProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
useCallback for Stable Function References
import { useCallback, useState } from 'react';
function ShoppingCart() {
const [items, setItems] = useState([]);
// Without useCallback - new function on every render
// const handleRemove = (id) => {
// setItems(items => items.filter(item => item.id !== id));
// };
// With useCallback - stable reference
const handleRemove = useCallback((id) => {
setItems(items => items.filter(item => item.id !== id));
}, []); // Empty deps - function never changes
const handleUpdateQuantity = useCallback((id, quantity) => {
setItems(items => items.map(item =>
item.id === id ? { ...item, quantity } : item
));
}, []);
return (
<div>
{items.map(item => (
<CartItem
key={item.id}
item={item}
onRemove={handleRemove}
onUpdateQuantity={handleUpdateQuantity}
/>
))}
</div>
);
}
// CartItem can now be memoized effectively
const CartItem = memo(function CartItem({ item, onRemove, onUpdateQuantity }) {
return (
<div className="cart-item">
<span>{item.name}</span>
<input
type="number"
value={item.quantity}
onChange={(e) => onUpdateQuantity(item.id, parseInt(e.target.value))}
/>
<button onClick={() => onRemove(item.id)}>Remove</button>
</div>
);
});
Only use useMemo and useCallback when you have measured a performance problem. Premature optimization can make code harder to read without meaningful benefits.
Code Splitting and Lazy Loading
Route-Based Code Splitting
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Lazy load route components
const Home = lazy(() => import('./pages/Home'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const Checkout = lazy(() => import('./pages/Checkout'));
const Admin = lazy(() => import('./pages/Admin'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/checkout" element={<Checkout />} />
<Route path="/admin/*" element={<Admin />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
function PageLoader() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-500" />
</div>
);
}
Component-Level Code Splitting
import { lazy, Suspense, useState } from 'react';
// Heavy components loaded on demand
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
const ChartDashboard = lazy(() => import('./components/ChartDashboard'));
const ImageGallery = lazy(() => import('./components/ImageGallery'));
function ProductEditor({ product }) {
const [showCharts, setShowCharts] = useState(false);
return (
<div>
<h1>Edit Product</h1>
{/* Editor loads when component mounts */}
<Suspense fallback={<div>Loading editor...</div>}>
<RichTextEditor
content={product.description}
onChange={handleDescriptionChange}
/>
</Suspense>
{/* Charts only load when user clicks */}
<button onClick={() => setShowCharts(true)}>
Show Analytics
</button>
{showCharts && (
<Suspense fallback={<div>Loading charts...</div>}>
<ChartDashboard productId={product.id} />
</Suspense>
)}
</div>
);
}
Virtualization for Large Lists
Using react-window for Efficient Rendering
import { FixedSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
function VirtualizedProductList({ products }) {
const Row = ({ index, style }) => {
const product = products[index];
return (
<div style={style} className="flex items-center p-4 border-b">
<img
src={product.thumbnail}
alt={product.name}
className="w-16 h-16 object-cover rounded"
/>
<div className="ml-4">
<h3 className="font-semibold">{product.name}</h3>
<p className="text-gray-600">${product.price}</p>
</div>
</div>
);
};
return (
<div className="h-[600px] w-full">
<AutoSizer>
{({ height, width }) => (
<List
height={height}
width={width}
itemCount={products.length}
itemSize={80}
>
{Row}
</List>
)}
</AutoSizer>
</div>
);
}
Variable Size List for Dynamic Content
import { VariableSizeList as List } from 'react-window';
import { useRef, useCallback } from 'react';
function ChatMessages({ messages }) {
const listRef = useRef();
const rowHeights = useRef({});
const getRowHeight = useCallback((index) => {
return rowHeights.current[index] || 60;
}, []);
const setRowHeight = useCallback((index, size) => {
rowHeights.current[index] = size;
listRef.current?.resetAfterIndex(index);
}, []);
const Row = ({ index, style }) => {
const rowRef = useRef();
const message = messages[index];
useEffect(() => {
if (rowRef.current) {
setRowHeight(index, rowRef.current.getBoundingClientRect().height);
}
}, [index, setRowHeight]);
return (
<div style={style}>
<div ref={rowRef} className="p-4">
<p className="font-semibold">{message.sender}</p>
<p>{message.content}</p>
<span className="text-xs text-gray-500">{message.timestamp}</span>
</div>
</div>
);
};
return (
<List
ref={listRef}
height={500}
width="100%"
itemCount={messages.length}
itemSize={getRowHeight}
estimatedItemSize={60}
>
{Row}
</List>
);
}
State Management Optimization
Avoiding Unnecessary Context Re-renders
import { createContext, useContext, useState, useMemo } from 'react';
// Split context by update frequency
const UserContext = createContext();
const UserActionsContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState(null);
// Memoize actions to prevent re-renders
const actions = useMemo(() => ({
login: async (credentials) => {
const user = await authService.login(credentials);
setUser(user);
},
logout: () => {
authService.logout();
setUser(null);
},
updateProfile: async (data) => {
const updated = await userService.update(data);
setUser(updated);
},
}), []);
return (
<UserContext.Provider value={user}>
<UserActionsContext.Provider value={actions}>
{children}
</UserActionsContext.Provider>
</UserContext.Provider>
);
}
// Components only subscribe to what they need
function UserAvatar() {
const user = useContext(UserContext); // Only re-renders when user changes
return <img src={user?.avatar} alt={user?.name} />;
}
function LogoutButton() {
const { logout } = useContext(UserActionsContext); // Never re-renders!
return <button onClick={logout}>Logout</button>;
}
Using Zustand for Efficient State
import { create } from 'zustand';
import { shallow } from 'zustand/shallow';
const useStore = create((set, get) => ({
products: [],
cart: [],
filters: { category: 'all', maxPrice: 1000 },
setProducts: (products) => set({ products }),
addToCart: (product) => set((state) => ({
cart: [...state.cart, { ...product, quantity: 1 }]
})),
updateFilters: (filters) => set((state) => ({
filters: { ...state.filters, ...filters }
})),
// Computed values
get cartTotal() {
return get().cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
},
}));
// Select only needed state to prevent unnecessary re-renders
function CartIcon() {
const itemCount = useStore((state) => state.cart.length);
return <span className="badge">{itemCount}</span>;
}
function ProductFilters() {
// Use shallow comparison for object selections
const { filters, updateFilters } = useStore(
(state) => ({ filters: state.filters, updateFilters: state.updateFilters }),
shallow
);
return (
<div>
<select
value={filters.category}
onChange={(e) => updateFilters({ category: e.target.value })}
>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
</div>
);
}
Image Optimization
Lazy Loading Images
function OptimizedImage({ src, alt, className }) {
const [isLoaded, setIsLoaded] = useState(false);
const [isInView, setIsInView] = useState(false);
const imgRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.disconnect();
}
},
{ rootMargin: '100px' }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={imgRef} className={`relative ${className}`}>
{/* Placeholder */}
{!isLoaded && (
<div className="absolute inset-0 bg-gray-200 animate-pulse" />
)}
{/* Actual image */}
{isInView && (
<img
src={src}
alt={alt}
className={`transition-opacity duration-300 ${
isLoaded ? 'opacity-100' : 'opacity-0'
}`}
onLoad={() => setIsLoaded(true)}
loading="lazy"
/>
)}
</div>
);
}
Profiling and Measuring Performance
Using React DevTools Profiler
import { Profiler } from 'react';
function onRenderCallback(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) {
// Log slow renders
if (actualDuration > 16) { // More than one frame (60fps)
console.warn(`Slow render in ${id}:`, {
phase,
actualDuration: `${actualDuration.toFixed(2)}ms`,
baseDuration: `${baseDuration.toFixed(2)}ms`,
});
}
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Header />
<Profiler id="ProductList" onRender={onRenderCallback}>
<ProductList />
</Profiler>
<Footer />
</Profiler>
);
}
Custom Performance Hooks
function useRenderCount(componentName) {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
console.log(`${componentName} rendered ${renderCount.current} times`);
});
}
function useWhyDidYouUpdate(name, props) {
const previousProps = useRef();
useEffect(() => {
if (previousProps.current) {
const allKeys = Object.keys({ ...previousProps.current, ...props });
const changedProps = {};
allKeys.forEach(key => {
if (previousProps.current[key] !== props[key]) {
changedProps[key] = {
from: previousProps.current[key],
to: props[key],
};
}
});
if (Object.keys(changedProps).length) {
console.log(`[${name}] Changed props:`, changedProps);
}
}
previousProps.current = props;
});
}
Conclusion
React performance optimization is about understanding the rendering lifecycle and applying the right techniques at the right time. Start by measuring with React DevTools Profiler, identify bottlenecks, and apply targeted optimizations.
Key takeaways:
- Use
React.memo,useMemo, anduseCallbackstrategically - Implement code splitting for large applications
- Virtualize long lists
- Optimize state management to minimize re-renders
- Always measure before and after optimization
Top comments (0)