June 3, 2024
12 min read
React Performance Optimization: Advanced Techniques
React
Performance
JavaScript
Optimization
Building performant React applications requires understanding and implementing various optimization techniques. This guide covers advanced strategies to make your React apps lightning-fast.
Understanding React Performance
React Rendering Process
React's rendering process involves several phases:
- Trigger: State change or prop update
- Render: Component re-execution and virtual DOM creation
- Commit: DOM updates and effect execution
Performance Measurement Tools
// React DevTools Profiler
import { Profiler } from 'react';
function App() {
const onRenderCallback = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
console.log('Profiler data:', {
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
});
};
return (
<Profiler id="App" onRender={onRenderCallback}>
<MyComponents />
</Profiler>
);
}
// Performance API
function measurePerformance(name, fn) {
return (...args) => {
performance.mark(`${name}-start`);
const result = fn(...args);
performance.mark(`${name}-end`);
performance.measure(name, `${name}-start`, `${name}-end`);
const measure = performance.getEntriesByName(name)[0];
console.log(`${name} took ${measure.duration} milliseconds`);
return result;
};
}
// Web Vitals measurement
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
// Send to your analytics service
console.log(metric);
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
Memoization Strategies
React.memo for Component Memoization
import React, { memo, useState, useMemo, useCallback } from 'react';
// Basic memoization
const ExpensiveComponent = memo(({ data, onUpdate }) => {
console.log('ExpensiveComponent rendering');
return (
<div>
{data.map(item => (
<div key={item.id}>{item.name}</div>
))}
<button onClick={onUpdate}>Update</button>
</div>
);
});
// Custom comparison function
const SmartComponent = memo(({ user, settings }) => {
return (
<div>
<h2>{user.name}</h2>
<p>{settings.theme}</p>
</div>
);
}, (prevProps, nextProps) => {
// Only re-render if user.name or settings.theme changes
return (
prevProps.user.name === nextProps.user.name &&
prevProps.settings.theme === nextProps.settings.theme
);
});
// Parent component with optimized callbacks
function ParentComponent() {
const [data, setData] = useState([]);
const [count, setCount] = useState(0);
// Memoized callback to prevent unnecessary re-renders
const handleUpdate = useCallback(() => {
setData(prevData => [...prevData, { id: Date.now(), name: `Item ${Date.now()}` }]);
}, []);
// Expensive calculation memoized
const expensiveValue = useMemo(() => {
console.log('Calculating expensive value');
return data.reduce((sum, item) => sum + item.id, 0);
}, [data]);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<p>Expensive value: {expensiveValue}</p>
<ExpensiveComponent data={data} onUpdate={handleUpdate} />
</div>
);
}
Advanced useMemo Patterns
import { useMemo, useRef, useEffect } from 'react';
// Memoizing complex objects
function useComplexCalculation(dependencies) {
return useMemo(() => {
// Expensive calculation
const result = dependencies.reduce((acc, dep) => {
return {
...acc,
[dep.key]: dep.value * 2 + Math.random()
};
}, {});
return result;
}, [dependencies]);
}
// Memoizing with deep comparison
function useDeepMemo(value, deps) {
const ref = useRef();
const signalRef = useRef(0);
const isEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
if (!isEqual(value, ref.current)) {
ref.current = value;
signalRef.current += 1;
}
return useMemo(() => ref.current, [signalRef.current, ...deps]);
}
// Selective memoization based on conditions
function ConditionalMemoComponent({ data, shouldOptimize }) {
const processedData = useMemo(() => {
if (!shouldOptimize) return data;
console.log('Processing data...');
return data
.filter(item => item.active)
.sort((a, b) => a.priority - b.priority)
.map(item => ({ ...item, processed: true }));
}, shouldOptimize ? [data] : []);
return (
<div>
{(shouldOptimize ? processedData : data).map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
Code Splitting and Lazy Loading
Dynamic Imports and React.lazy
import React, { Suspense, lazy } from 'react';
// Lazy load components
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
// Component-level code splitting
function App() {
const [currentView, setCurrentView] = useState('home');
return (
<div>
<nav>
<button onClick={() => setCurrentView('dashboard')}>Dashboard</button>
<button onClick={() => setCurrentView('profile')}>Profile</button>
</nav>
<Suspense fallback={<div>Loading...</div>}>
{currentView === 'dashboard' && <Dashboard />}
{currentView === 'profile' && <Profile />}
</Suspense>
</div>
);
}
// Route-based code splitting with React Router
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
function AppRouter() {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
// Preloading strategies
function usePreloadRoute(routeComponent) {
useEffect(() => {
const preload = () => {
// Preload on hover or other interactions
routeComponent();
};
return preload;
}, [routeComponent]);
}
// Smart loading with error boundaries
class LazyErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Lazy loading error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>Failed to load component</h2>
<button onClick={() => this.setState({ hasError: false })}>
Retry
</button>
</div>
);
}
return this.props.children;
}
}
Bundle Analysis and Optimization
// webpack-bundle-analyzer configuration
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
})
],
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
enforce: true,
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
},
},
},
};
// Dynamic import with error handling
async function loadModule(moduleName) {
try {
const module = await import(/* webpackChunkName: "[request]" */ `./modules/${moduleName}`);
return module.default;
} catch (error) {
console.error(`Failed to load module ${moduleName}:`, error);
// Fallback or retry logic
return null;
}
}
Virtual Scrolling and Large Lists
Implementing Virtual Scrolling
import React, { useState, useEffect, useRef, useMemo } from 'react';
function VirtualList({ items, itemHeight, containerHeight, renderItem }) {
const [scrollTop, setScrollTop] = useState(0);
const scrollElementRef = useRef();
const visibleItems = useMemo(() => {
const containerScrollTop = scrollTop;
const startIndex = Math.floor(containerScrollTop / itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / itemHeight) + 1,
items.length
);
return items.slice(startIndex, endIndex).map((item, index) => ({
...item,
index: startIndex + index,
}));
}, [scrollTop, items, itemHeight, containerHeight]);
const totalHeight = items.length * itemHeight;
const offsetY = Math.floor(scrollTop / itemHeight) * itemHeight;
return (
<div
ref={scrollElementRef}
style={{ height: containerHeight, overflowY: 'auto' }}
onScroll={(e) => setScrollTop(e.target.scrollTop)}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item) => (
<div
key={item.index}
style={{ height: itemHeight }}
>
{renderItem(item, item.index)}
</div>
))}
</div>
</div>
</div>
);
}
// Usage with react-window (recommended library)
import { FixedSizeList as List } from 'react-window';
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
<div style={{ padding: '10px' }}>
{items[index].name} - {items[index].description}
</div>
</div>
);
return (
<List
height={400}
itemCount={items.length}
itemSize={60}
width="100%"
>
{Row}
</List>
);
}
// Infinite scrolling with virtual list
function InfiniteVirtualList({ loadMore, hasNextPage, isLoading }) {
const [items, setItems] = useState([]);
const loadMoreItems = useCallback(async () => {
if (!isLoading && hasNextPage) {
const newItems = await loadMore();
setItems(prev => [...prev, ...newItems]);
}
}, [loadMore, hasNextPage, isLoading]);
const Row = ({ index, style }) => {
const item = items[index];
// Load more when reaching near the end
if (index === items.length - 5) {
loadMoreItems();
}
return (
<div style={style}>
{item ? (
<div>{item.name}</div>
) : (
<div>Loading...</div>
)}
</div>
);
};
return (
<List
height={400}
itemCount={hasNextPage ? items.length + 1 : items.length}
itemSize={60}
width="100%"
>
{Row}
</List>
);
}
State Management Optimization
Reducing Re-renders with State Structure
// Poor state structure - causes unnecessary re-renders
function BadExample() {
const [state, setState] = useState({
user: { name: '', email: '' },
posts: [],
comments: [],
ui: { loading: false, error: null }
});
// This re-renders everything when loading changes
const setLoading = (loading) => {
setState(prev => ({ ...prev, ui: { ...prev.ui, loading } }));
};
return <div>...</div>;
}
// Better state structure - granular updates
function GoodExample() {
const [user, setUser] = useState({ name: '', email: '' });
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
return <div>...</div>;
}
// Context optimization with multiple contexts
const UserContext = createContext();
const PostsContext = createContext();
const UIContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [ui, setUI] = useState({ loading: false, error: null });
return (
<UserContext.Provider value={{ user, setUser }}>
<PostsContext.Provider value={{ posts, setPosts }}>
<UIContext.Provider value={{ ui, setUI }}>
{children}
</UIContext.Provider>
</PostsContext.Provider>
</UserContext.Provider>
);
}
// Zustand for optimized state management
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
const useStore = create(
subscribeWithSelector((set, get) => ({
user: null,
posts: [],
comments: [],
setUser: (user) => set({ user }),
addPost: (post) => set((state) => ({ posts: [...state.posts, post] })),
// Computed values
get userPosts() {
const { user, posts } = get();
return posts.filter(post => post.authorId === user?.id);
},
}))
);
// Selective subscriptions
function UserProfile() {
const user = useStore((state) => state.user);
const setUser = useStore((state) => state.setUser);
// This component only re-renders when user changes
return <div>{user?.name}</div>;
}
Image and Asset Optimization
Lazy Loading Images
import React, { useState, useRef, useEffect } from 'react';
function LazyImage({ src, alt, placeholder, ...props }) {
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();
}
},
{ threshold: 0.1 }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={imgRef} {...props}>
{isInView && (
<img
src={src}
alt={alt}
onLoad={() => setIsLoaded(true)}
style={{
opacity: isLoaded ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
/>
)}
{!isLoaded && placeholder && (
<div style={{ backgroundColor: '#f0f0f0', ...props.style }}>
{placeholder}
</div>
)}
</div>
);
}
// Progressive image loading
function ProgressiveImage({ lowQualitySrc, highQualitySrc, alt }) {
const [currentSrc, setCurrentSrc] = useState(lowQualitySrc);
const [isHighQualityLoaded, setIsHighQualityLoaded] = useState(false);
useEffect(() => {
const img = new Image();
img.onload = () => {
setCurrentSrc(highQualitySrc);
setIsHighQualityLoaded(true);
};
img.src = highQualitySrc;
}, [highQualitySrc]);
return (
<img
src={currentSrc}
alt={alt}
style={{
filter: isHighQualityLoaded ? 'none' : 'blur(2px)',
transition: 'filter 0.3s ease-in-out',
}}
/>
);
}
// WebP support detection
function OptimizedImage({ src, webpSrc, alt, ...props }) {
const [supportsWebP, setSupportsWebP] = useState(false);
useEffect(() => {
const checkWebPSupport = () => {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
return canvas.toDataURL('image/webp').indexOf('image/webp') === 5;
};
setSupportsWebP(checkWebPSupport());
}, []);
return (
<img
src={supportsWebP && webpSrc ? webpSrc : src}
alt={alt}
{...props}
/>
);
}
Advanced Patterns
Render Props and Higher-Order Components
// Optimized render props pattern
function DataFetcher({ url, children, dependencies = [] }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}, [url, ...dependencies]);
useEffect(() => {
fetchData();
}, [fetchData]);
return children({ data, loading, error, refetch: fetchData });
}
// Memoized HOC
function withMemoization(WrappedComponent, propsAreEqual) {
const MemoizedComponent = memo(WrappedComponent, propsAreEqual);
return function WithMemoizationComponent(props) {
return <MemoizedComponent {...props} />;
};
}
// Performance monitoring HOC
function withPerformanceMonitoring(WrappedComponent, componentName) {
return function PerformanceMonitoredComponent(props) {
const renderStartTime = useRef();
renderStartTime.current = performance.now();
useEffect(() => {
const renderEndTime = performance.now();
const renderDuration = renderEndTime - renderStartTime.current;
if (renderDuration > 16) { // Longer than one frame
console.warn(`${componentName} took ${renderDuration}ms to render`);
}
});
return <WrappedComponent {...props} />;
};
}
Custom Optimization Hooks
// Debounced state
function useDebouncedState(initialValue, delay) {
const [value, setValue] = useState(initialValue);
const [debouncedValue, setDebouncedValue] = useState(initialValue);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return [debouncedValue, setValue];
}
// Throttled callback
function useThrottledCallback(callback, delay) {
const throttledCallback = useRef(null);
return useCallback((...args) => {
if (!throttledCallback.current) {
callback(...args);
throttledCallback.current = setTimeout(() => {
throttledCallback.current = null;
}, delay);
}
}, [callback, delay]);
}
// Intersection observer hook
function useIntersectionObserver(options = {}) {
const [isIntersecting, setIsIntersecting] = useState(false);
const [node, setNode] = useState(null);
useEffect(() => {
if (!node) return;
const observer = new IntersectionObserver(
([entry]) => setIsIntersecting(entry.isIntersecting),
options
);
observer.observe(node);
return () => observer.disconnect();
}, [node, options]);
return [setNode, isIntersecting];
}
Production Optimization Checklist
Build Optimization
// Next.js optimization
module.exports = {
// Image optimization
images: {
domains: ['example.com'],
formats: ['image/webp', 'image/avif'],
},
// Bundle analyzer
webpack: (config, { isServer }) => {
if (process.env.ANALYZE) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
config.plugins.push(new BundleAnalyzerPlugin());
}
return config;
},
// Experimental features
experimental: {
optimizeCss: true,
modern: true,
},
};
// Vite optimization
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
ui: ['@mui/material'],
},
},
},
},
plugins: [
react(),
splitVendorChunkPlugin(),
],
});
Conclusion
React performance optimization is crucial for delivering excellent user experiences. Key strategies include:
- Memoization: Use React.memo, useMemo, and useCallback strategically
- Code Splitting: Implement lazy loading and route-based splitting
- Virtual Scrolling: Handle large lists efficiently
- State Optimization: Structure state to minimize re-renders
- Asset Optimization: Lazy load images and use modern formats
- Monitoring: Continuously measure and monitor performance
Remember that premature optimization can be counterproductive. Always measure first, identify bottlenecks, then apply appropriate optimization techniques. Use React DevTools Profiler to identify performance issues and validate your optimizations.