React Performance Optimization: Complete Guide to Building Fast Applications
These articles are AI-generated summaries. Please check the original sources for full details.
React Performance Optimization: Complete Guide to Building Fast Applications
React applications can become slow as they grow. This comprehensive guide covers everything you need to know about optimizing React applications for maximum performance, from basic techniques to advanced patterns used in production by top companies.
Table of Contents
- Understanding React Performance
- Profiling and Measuring Performance
- Component Optimization
- Code Splitting and Lazy Loading
- State Management Optimization
- Rendering Optimization
- Bundle Size Optimization
- Advanced Patterns
- Real-World Case Studies
Understanding React Performance {#understanding-performance}
Performance Metrics That Matter
Core Web Vitals:
- LCP (Largest Contentful Paint): < 2.5s
- FID (First Input Delay): < 100ms
- CLS (Cumulative Layout Shift): < 0.1
- TTI (Time to Interactive): < 3.8s
React-Specific Metrics:
- Component render time
- Re-render frequency
- Bundle size
- Initial load time
- JavaScript execution time
How React Rendering Works
// React's rendering phases
1. Trigger → 2. Render → 3. Commit → 4. Browser Paint
// Understanding the render phase
function ParentComponent() {
const [count, setCount] = useState(0);
// Every state update triggers a re-render
// All child components re-render by default!
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<ChildComponent /> {/* Re-renders unnecessarily */}
<AnotherChild /> {/* Re-renders unnecessarily */}
</div>
);
}
Profiling and Measuring Performance {#profiling}
1. React DevTools Profiler
import { Profiler } from 'react';
function onRenderCallback(
id, // Component ID
phase, // "mount" or "update"
actualDuration, // Time spent rendering
baseDuration, // Estimated time without memoization
startTime,
commitTime,
interactions
) {
console.log(`${id} took ${actualDuration}ms to render`);
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<MainContent />
</Profiler>
);
}
2. Performance API
import { useEffect } from 'react';
function PerformanceMonitor() {
useEffect(() => {
// Measure component mount time
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('Performance entry:', {
name: entry.name,
duration: entry.duration,
startTime: entry.startTime
});
}
});
observer.observe({ entryTypes: ['measure', 'navigation'] });
return () => observer.disconnect();
}, []);
return null;
}
3. Custom Performance Hook
function usePerformance(componentName) {
useEffect(() => {
const startTime = performance.now();
return () => {
const endTime = performance.now();
const renderTime = endTime - startTime;
if (renderTime > 16.67) { // > 60fps threshold
console.warn(
`${componentName} took ${renderTime}ms to render`
);
}
};
});
}
// Usage
function ExpensiveComponent() {
usePerformance('ExpensiveComponent');
// Component logic...
}
Component Optimization {#component-optimization}
1. React.memo for Preventing Unnecessary Re-renders
// ❌ Bad: Re-renders on every parent update
function ExpensiveChild({ data }) {
console.log('Child rendered');
return <div>{expensiveComputation(data)}</div>;
}
// ✅ Good: Only re-renders when data changes
const ExpensiveChild = React.memo(function ExpensiveChild({ data }) {
console.log('Child rendered');
return <div>{expensiveComputation(data)}</div>;
});
// ✅ Better: Custom comparison function
const ExpensiveChild = React.memo(
function ExpensiveChild({ user, metadata }) {
return (
<div>
{user.name} - {metadata.lastSeen}
</div>
);
},
(prevProps, nextProps) => {
// Only re-render if user.id changes
return prevProps.user.id === nextProps.user.id;
}
);
2. useMemo for Expensive Calculations
function DataTable({ items, filters }) {
// ❌ Bad: Recalculates on every render
const filteredItems = items.filter(item =>
filters.every(filter => filter(item))
);
// ✅ Good: Only recalculates when dependencies change
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
filters.every(filter => filter(item))
);
}, [items, filters]);
// ✅ Complex example with sorting
const sortedAndFilteredData = useMemo(() => {
const filtered = items.filter(item =>
item.active && item.score > 50
);
return filtered.sort((a, b) =>
b.score - a.score
).slice(0, 100);
}, [items]);
return (
<div>
{filteredItems.map(item => (
<ItemRow key={item.id} item={item} />
))}
</div>
);
}
3. useCallback for Function Memoization
function SearchComponent() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
// ❌ Bad: Creates new function on every render
const handleSearch = (value) => {
fetchResults(value).then(setResults);
};
// ✅ Good: Stable function reference
const handleSearch = useCallback((value) => {
fetchResults(value).then(setResults);
}, []); // Empty deps if function is self-contained
// ✅ With dependencies
const handleSearchWithFilter = useCallback((value) => {
fetchResults(value, query).then(setResults);
}, [query]); // Recreate when query changes
return (
<SearchInput
onSearch={handleSearch}
placeholder="Search..."
/>
);
}
// Child component benefits from stable function
const SearchInput = React.memo(({ onSearch, placeholder }) => {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
onSearch(e.target.value);
}}
placeholder={placeholder}
/>
);
});
4. Debouncing and Throttling
import { useCallback, useRef, useEffect } from 'react';
// Custom debounce hook
function useDebounce(callback, delay) {
const timeoutRef = useRef(null);
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
const debouncedCallback = useCallback((...args) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callback(...args);
}, delay);
}, [callback, delay]);
return debouncedCallback;
}
// Usage in search
function SearchWithDebounce() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const search = async (searchTerm) => {
const data = await fetchResults(searchTerm);
setResults(data);
};
const debouncedSearch = useDebounce(search, 300);
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value); // Only calls API after 300ms of no typing
};
return (
<div>
<input
value={query}
onChange={handleChange}
placeholder="Search..."
/>
<Results data={results} />
</div>
);
}
// Throttle hook for scroll events
function useThrottle(callback, limit) {
const inThrottle = useRef(false);
const throttledCallback = useCallback((...args) => {
if (!inThrottle.current) {
callback(...args);
inThrottle.current = true;
setTimeout(() => {
inThrottle.current = false;
}, limit);
}
}, [callback, limit]);
return throttledCallback;
}
// Usage in infinite scroll
function InfiniteScrollList() {
const loadMore = async () => {
// Load more items
};
const throttledLoadMore = useThrottle(loadMore, 1000);
useEffect(() => {
const handleScroll = () => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 500) {
throttledLoadMore();
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [throttledLoadMore]);
return <div>{/* List items */}</div>;
}
Code Splitting and Lazy Loading {#code-splitting}
1. Route-Based Code Splitting
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// ❌ Bad: All routes loaded upfront
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';
import Profile from './pages/Profile';
import Settings from './pages/Settings';
// ✅ Good: Lazy load route components
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
2. Component-Based Code Splitting
// Lazy load heavy components
const Chart = lazy(() => import('./components/Chart'));
const DataTable = lazy(() => import('./components/DataTable'));
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<h1>Dashboard</h1>
<button onClick={() => setShowChart(true)}>
Show Chart
</button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<Chart data={data} />
</Suspense>
)}
</div>
);
}
3. Preloading Components
// Preload on hover for better UX
const Settings = lazy(() => import('./pages/Settings'));
function Navigation() {
const preloadSettings = () => {
// Preload the component
const component = import('./pages/Settings');
};
return (
<nav>
<Link
to="/settings"
onMouseEnter={preloadSettings}
onFocus={preloadSettings}
>
Settings
</Link>
</nav>
);
}
4. Dynamic Imports with Webpack Magic Comments
// Prefetch: Load during idle time
const AdminPanel = lazy(() =>
import(
/* webpackChunkName: "admin" */
/* webpackPrefetch: true */
'./pages/AdminPanel'
)
);
// Preload: Load in parallel with parent
const CriticalComponent = lazy(() =>
import(
/* webpackChunkName: "critical" */
/* webpackPreload: true */
'./components/CriticalComponent'
)
);
State Management Optimization {#state-management}
1. State Colocation
// ❌ Bad: State in parent, causing unnecessary re-renders
function Parent() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [address, setAddress] = useState('');
return (
<div>
<NameInput value={name} onChange={setName} />
<EmailInput value={email} onChange={setEmail} />
<AddressInput value={address} onChange={setAddress} />
<ExpensiveList /> {/* Re-renders unnecessarily */}
</div>
);
}
// ✅ Good: State colocated with component that needs it
function NameInput() {
const [name, setName] = useState('');
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
}
function Parent() {
return (
<div>
<NameInput />
<EmailInput />
<AddressInput />
<ExpensiveList /> {/* Doesn't re-render */}
</div>
);
}
2. Context Optimization
// ❌ Bad: Single context causes all consumers to re-render
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
return (
<AppContext.Provider value={{
user, setUser,
theme, setTheme,
notifications, setNotifications
}}>
{children}
</AppContext.Provider>
);
}
// ✅ Good: Split contexts by update frequency
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationsContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<NotificationsContext.Provider value={{ notifications, setNotifications }}>
{children}
</NotificationsContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}
// ✅ Better: Use composition to prevent re-renders
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const userValue = useMemo(() => ({ user, setUser }), [user]);
return (
<UserContext.Provider value={userValue}>
{children}
</UserContext.Provider>
);
}
3. Zustand for Efficient State Management
import create from 'zustand';
// Create store with minimal re-renders
const useStore = create((set) => ({
// State
user: null,
todos: [],
filter: 'all',
// Actions
setUser: (user) => set({ user }),
addTodo: (todo) => set((state) => ({
todos: [...state.todos, todo]
})),
setFilter: (filter) => set({ filter }),
// Computed values
get filteredTodos() {
const { todos, filter } = this;
if (filter === 'completed') {
return todos.filter(t => t.completed);
}
return todos;
}
}));
// Component only re-renders when user changes
function UserProfile() {
const user = useStore((state) => state.user);
return <div>{user?.name}</div>;
}
// Component only re-renders when todos change
function TodoList() {
const todos = useStore((state) => state.filteredTodos);
return todos.map(todo => <TodoItem key={todo.id} todo={todo} />);
}
Rendering Optimization {#rendering-optimization}
1. Virtualization for Long Lists
import { FixedSizeList } from 'react-window';
// ❌ Bad: Renders all 10,000 items
function BadList({ items }) {
return (
<div>
{items.map((item) => (
<div key={item.id} style={{ height: 50 }}>
{item.name}
</div>
))}
</div>
);
}
// ✅ Good: Only renders visible items
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
// ✅ Variable size items
import { VariableSizeList } from 'react-window';
function VariableSizeVirtualList({ items }) {
const getItemSize = (index) => {
// Dynamic height based on content
return items[index].isExpanded ? 120 : 50;
};
const Row = ({ index, style }) => (
<div style={style}>
<ItemCard item={items[index]} />
</div>
);
return (
<VariableSizeList
height={600}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{Row}
</VariableSizeList>
);
}
2. React Window with AutoSizer
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
function ResponsiveVirtualList({ items }) {
return (
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
height={height}
itemCount={items.length}
itemSize={50}
width={width}
>
{({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
)}
</FixedSizeList>
)}
</AutoSizer>
);
}
3. Windowing with Infinite Scroll
import { useInfiniteQuery } from '@tanstack/react-query';
import { FixedSizeList } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
function InfiniteVirtualList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['items'],
queryFn: ({ pageParam = 0 }) => fetchItems(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor
});
const items = data?.pages.flatMap(page => page.items) ?? [];
const loadMoreItems = isFetchingNextPage
? () => {}
: () => fetchNextPage();
const isItemLoaded = (index) => !hasNextPage || index < items.length;
return (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={hasNextPage ? items.length + 1 : items.length}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
onItemsRendered={onItemsRendered}
ref={ref}
width="100%"
>
{({ index, style }) => (
<div style={style}>
{items[index]?.name ?? 'Loading...'}
</div>
)}
</FixedSizeList>
)}
</InfiniteLoader>
);
}
Bundle Size Optimization {#bundle-size}
1. Analyzing Bundle Size
# Install bundle analyzer
npm install --save-dev webpack-bundle-analyzer
# Add to package.json
"scripts": {
"analyze": "ANALYZE=true npm run build"
}
# Create custom webpack config (Create React App)
npm install --save-dev @craco/craco
// craco.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
webpack: {
plugins: [
...(process.env.ANALYZE === 'true'
? [new BundleAnalyzerPlugin()]
: [])
]
}
};
2. Tree Shaking
// ❌ Bad: Imports entire library
import _ from 'lodash';
import { format } from 'date-fns';
// ✅ Good: Import only what you need
import debounce from 'lodash/debounce';
import map from 'lodash/map';
import format from 'date-fns/format';
// ✅ Better: Use tree-shakeable libraries
import { debounce, map } from 'lodash-es';
3. Dynamic Imports for Heavy Libraries
// Lazy load heavy chart library
function ChartComponent({ data }) {
const [Chart, setChart] = useState(null);
useEffect(() => {
import('recharts').then((module) => {
setChart(() => module.LineChart);
});
}, []);
if (!Chart) return <ChartSkeleton />;
return <Chart data={data} />;
}
// Or use lazy
const Chart = lazy(() =>
import('recharts').then(module => ({
default: module.LineChart
}))
);
Advanced Patterns {#advanced-patterns}
1. Concurrent Rendering (React 18)
import { startTransition } from 'react';
function SearchComponent() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleSearch = (value) => {
// High priority: Update input immediately
setQuery(value);
// Low priority: Update results (can be interrupted)
startTransition(() => {
const filtered = expensiveFilter(items, value);
setResults(filtered);
});
};
return (
<>
<input
value={query}
onChange={(e) => handleSearch(e.target.value)}
/>
<ResultsList results={results} />
</>
);
}
2. useDeferredValue
import { useDeferredValue, useMemo } from 'react';
function ProductSearch() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => {
// Expensive search operation
return searchProducts(deferredQuery);
}, [deferredQuery]);
return (
<>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
{query !== deferredQuery && <LoadingSpinner />}
<ProductList products={results} />
</>
);
}
3. Web Workers for Heavy Computations
// worker.js
self.addEventListener('message', (e) => {
const { data, type } = e.data;
if (type === 'PROCESS_DATA') {
const result = heavyComputation(data);
self.postMessage({ type: 'RESULT', result });
}
});
function heavyComputation(data) {
// Complex calculations
return data.map(item => {
// Heavy processing
return processItem(item);
});
}
// useWorker.js
import { useEffect, useRef, useState } from 'react';
function useWorker(workerPath) {
const workerRef = useRef(null);
const [result, setResult] = useState(null);
const [isProcessing, setIsProcessing] = useState(false);
useEffect(() => {
workerRef.current = new Worker(workerPath);
workerRef.current.onmessage = (e) => {
if (e.data.type === 'RESULT') {
setResult(e.data.result);
setIsProcessing(false);
}
};
return () => workerRef.current?.terminate();
}, [workerPath]);
const process = (data) => {
setIsProcessing(true);
workerRef.current?.postMessage({
type: 'PROCESS_DATA',
data
});
};
return { result, isProcessing, process };
}
// Usage
function DataProcessor() {
const { result, isProcessing, process } = useWorker('/worker.js');
const handleProcess = () => {
process(largeDataset);
};
return (
<div>
<button onClick={handleProcess} disabled={isProcessing}>
Process Data
</button>
{isProcessing && <LoadingSpinner />}
{result && <Results data={result} />}
</div>
);
}
Real-World Case Studies {#case-studies}
Case Study 1: Optimizing a Dashboard
Before:
- Initial load: 8.2s
- Time to Interactive: 12.5s
- Bundle size: 2.8MB
- Lighthouse score: 42/100
Optimizations Applied:
- ✅ Route-based code splitting
- ✅ Virtualized data tables
- ✅ Memoized expensive charts
- ✅ Lazy loaded modals and dialogs
- ✅ Optimized context providers
- ✅ Tree-shaken lodash imports
After:
- Initial load: 2.1s (74% improvement)
- Time to Interactive: 3.8s (70% improvement)
- Bundle size: 890KB (68% reduction)
- Lighthouse score: 94/100
Case Study 2: E-Commerce Product List
Challenge: Render 10,000 products with filtering
Solution:
import { FixedSizeGrid } from 'react-window';
function ProductGrid({ products }) {
const [filters, setFilters] = useState({});
const filteredProducts = useMemo(() => {
return products.filter(product => {
return Object.entries(filters).every(([key, value]) => {
return product[key] === value;
});
});
}, [products, filters]);
const Cell = ({ columnIndex, rowIndex, style }) => {
const index = rowIndex * 4 + columnIndex;
const product = filteredProducts[index];
if (!product) return null;
return (
<div style={style}>
<ProductCard product={product} />
</div>
);
};
return (
<FixedSizeGrid
columnCount={4}
columnWidth={250}
height={800}
rowCount={Math.ceil(filteredProducts.length / 4)}
rowHeight={350}
width={1040}
>
{Cell}
</FixedSizeGrid>
);
}
Results:
- Smooth 60fps scrolling
- Initial render: 150ms (vs 3.2s before)
- Memory usage: 45MB (vs 380MB before)
Performance Checklist
Development:
- Use React DevTools Profiler
- Implement useMemo for expensive calculations
- Use useCallback for event handlers
- Wrap components with React.memo
- Colocate state as close as possible
- Implement code splitting for routes
- Lazy load heavy components
- Virtualize long lists
Build:
- Analyze bundle size
- Remove unused dependencies
- Tree-shake libraries
- Enable production mode
- Minify and compress assets
- Use CDN for static assets
- Implement caching strategies
Runtime:
- Debounce/throttle frequent operations
- Use Web Workers for heavy computations
- Implement proper loading states
- Optimize images (WebP, lazy loading)
- Monitor Core Web Vitals
- Use service workers for offline support
Conclusion
React performance optimization is a continuous process. Key takeaways:
- Measure First - Use profiling tools before optimizing
- Start Simple - Basic optimizations have the biggest impact
- Avoid Premature Optimization - Focus on bottlenecks
- Test Real Scenarios - Optimize for actual user behavior
- Monitor Production - Track metrics over time
Remember: A fast React app is a successful React app.
Resources
What React performance techniques have worked best for you? Share your experiences!
Continue reading
Next article
Transitive RL: A Divide-and-Conquer Approach to Scalable Off-Policy Reinforcement Learning
Related Content
Frontend Performance Optimization: Complete Guide to Building Fast Web Apps
Master frontend performance optimization with proven techniques for loading speed, runtime performance, bundle size, critical rendering path, and Core Web Vitals. Complete guide with practical examples.
Elevating React Performance: A Deep Dive into Optimization Techniques
Optimize React applications for speed and responsiveness using techniques like memoization, code splitting, and virtualization, improving user experience.
Spring Boot Performance Optimization: Expert Tips and Techniques
Complete guide to optimizing Spring Boot applications for production. Learn JVM tuning, database optimization, caching strategies, and monitoring best practices to achieve high performance.