React Performance Optimization: 15 Proven Techniques 2026
Complete guide to React performance optimization. 15 proven techniques to make your React apps faster, including lazy loading, memoization, and code splitting.
React apps can become sluggish as they grow. After optimizing hundreds of React applications, I've identified the techniques that actually move the needle. This guide covers 15 proven methods to make your React apps lightning fast.
Related reading: Check out our guides on TypeScript migration and Docker development setup for more development insights.
Why React Performance Matters#
The Real Impact of Slow Apps#
User experience suffers:
- 53% of users abandon sites that take more than 3 seconds to load
- 1-second delay reduces conversions by 7%
- Slow apps get lower app store ratings
- Poor performance hurts SEO rankings
Business impact:
- Amazon loses $1.6B annually for every second of delay
- Google found 2% slower = 2% fewer searches
- Pinterest increased signups 15% by reducing load time 40%
Common React performance problems:
- Unnecessary re-renders
- Large bundle sizes
- Blocking JavaScript execution
- Memory leaks
- Inefficient state updates
How to Measure React Performance#
Essential Tools#
React Developer Tools:
# Install browser extension
# Chrome: React Developer Tools
# Firefox: React Developer Tools
# Enable Profiler
# Components tab > Profiler > Record
Web Vitals:
- LCP (Largest Contentful Paint): less than 2.5s
- FID (First Input Delay): less than 100ms
- CLS (Cumulative Layout Shift): less than 0.1
Performance measurement:
// Measure component render time
import { Profiler } from 'react';
function onRenderCallback(id, phase, actualDuration) {
console.log('Component:', id, 'Phase:', phase, 'Duration:', actualDuration);
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<MyComponent />
</Profiler>
);
}
Bundle analysis:
# Analyze bundle size
npm install --save-dev webpack-bundle-analyzer
npx webpack-bundle-analyzer build/static/js/*.js
15 React Performance Optimization Techniques#
1. Use React.memo for Component Memoization#
Problem: Components re-render even when props haven't changed.
Solution: Wrap components with React.memo.
// Before: Re-renders on every parent update
function ExpensiveComponent({ data, count }) {
console.log('Rendering ExpensiveComponent');
return <div>{data.map(item => <Item key={item.id} item={item} />)}</div>;
}
// After: Only re-renders when props change
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data, count }) {
console.log('Rendering ExpensiveComponent');
return <div>{data.map(item => <Item key={item.id} item={item} />)}</div>;
});
// Custom comparison function
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data, count }) {
return <div>{data.map(item => <Item key={item.id} item={item} />)}</div>;
}, (prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return prevProps.data.length === nextProps.data.length &&
prevProps.count === nextProps.count;
});
When to use:
- Components with expensive rendering
- Components that receive the same props frequently
- Leaf components in component tree
When NOT to use:
- Components that always receive different props
- Very simple components (overhead > benefit)
2. Optimize with useMemo and useCallback#
useMemo: Memoize expensive calculations.
// Before: Expensive calculation on every render
function ProductList({ products, searchTerm }) {
const filteredProducts = products.filter(product =>
product.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const sortedProducts = filteredProducts.sort((a, b) => a.price - b.price);
return <div>{sortedProducts.map(product => <Product key={product.id} product={product} />)}</div>;
}
// After: Memoize expensive operations
function ProductList({ products, searchTerm }) {
const filteredAndSortedProducts = useMemo(() => {
const filtered = products.filter(product =>
product.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return filtered.sort((a, b) => a.price - b.price);
}, [products, searchTerm]);
return <div>{filteredAndSortedProducts.map(product => <Product key={product.id} product={product} />)}</div>;
}
useCallback: Memoize function references.
// Before: New function on every render
function TodoList({ todos }) {
const handleToggle = (id) => {
// Toggle todo logic
};
return (
<div>
{todos.map(todo =>
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle} // New function reference every render
/>
)}
</div>
);
}
// After: Stable function reference
function TodoList({ todos }) {
const handleToggle = useCallback((id) => {
// Toggle todo logic
}, []); // Dependencies array
return (
<div>
{todos.map(todo =>
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle} // Same function reference
/>
)}
</div>
);
}
3. Implement Code Splitting with React.lazy#
Problem: Large bundle sizes slow initial load.
Solution: Split code into smaller chunks.
// Before: Everything loaded upfront
import Dashboard from './Dashboard';
import Profile from './Profile';
import Settings from './Settings';
function App() {
return (
<Router>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Router>
);
}
// After: Lazy load components
const Dashboard = React.lazy(() => import('./Dashboard'));
const Profile = React.lazy(() => import('./Profile'));
const Settings = React.lazy(() => import('./Settings'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</Router>
);
}
Advanced code splitting:
// Split by feature
const AdminPanel = React.lazy(() =>
import('./AdminPanel').then(module => ({ default: module.AdminPanel }))
);
// Conditional loading
const HeavyComponent = React.lazy(() => {
if (userRole === 'admin') {
return import('./AdminComponent');
}
return import('./UserComponent');
});
// Preload on hover
function NavLink({ to, children }) {
const handleMouseEnter = () => {
import('./Dashboard'); // Preload
};
return (
<Link to={to} onMouseEnter={handleMouseEnter}>
{children}
</Link>
);
}
4. Optimize List Rendering with Keys#
Problem: Inefficient list updates cause unnecessary re-renders.
Solution: Use stable, unique keys.
// Bad: Index as key
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo.text}</li> // Don't use index!
))}
</ul>
);
}
// Good: Unique, stable keys
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li> // Use unique ID
))}
</ul>
);
}
// Best: Optimized list component
const TodoItem = React.memo(function TodoItem({ todo, onToggle, onDelete }) {
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
});
function TodoList({ todos, onToggle, onDelete }) {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
}
5. Implement Virtual Scrolling for Large Lists#
Problem: Rendering thousands of items kills performance.
Solution: Only render visible items.
// Using react-window
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style, data }) => (
<div style={style}>
<div>Item {data[index].name}</div>
</div>
);
function VirtualizedList({ items }) {
return (
<List
height={600} // Container height
itemCount={items.length}
itemSize={50} // Each item height
itemData={items}
>
{Row}
</List>
);
}
// For variable height items
import { VariableSizeList as List } from 'react-window';
const getItemSize = (index) => {
// Return height for item at index
return items[index].height || 50;
};
function VariableList({ items }) {
return (
<List
height={600}
itemCount={items.length}
itemSize={getItemSize}
itemData={items}
>
{Row}
</List>
);
}
6. Optimize State Updates#
Problem: Frequent state updates cause performance issues.
Solution: Batch updates and optimize state structure.
// Bad: Multiple state updates
function Counter() {
const [count, setCount] = useState(0);
const [doubled, setDoubled] = useState(0);
const [tripled, setTripled] = useState(0);
const increment = () => {
setCount(c => c + 1); // Causes re-render
setDoubled(c => c + 2); // Causes re-render
setTripled(c => c + 3); // Causes re-render
};
return <button onClick={increment}>Count: {count}</button>;
}
// Good: Single state object
function Counter() {
const [state, setState] = useState({
count: 0,
doubled: 0,
tripled: 0
});
const increment = () => {
setState(prev => ({
count: prev.count + 1,
doubled: prev.count * 2,
tripled: prev.count * 3
})); // Single re-render
};
return <button onClick={increment}>Count: {state.count}</button>;
}
// Best: Use useReducer for complex state
function counterReducer(state, action) {
switch (action.type) {
case 'increment':
return {
count: state.count + 1,
doubled: (state.count + 1) * 2,
tripled: (state.count + 1) * 3
};
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, {
count: 0,
doubled: 0,
tripled: 0
});
return <button onClick={() => dispatch({ type: 'increment' })}>Count: {state.count}</button>;
}
7. Use Concurrent Features#
React 18 concurrent features improve perceived performance.
// Automatic batching (React 18)
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
// Automatically batched in React 18
setCount(c => c + 1);
setFlag(f => !f);
// Only one re-render
}
return <button onClick={handleClick}>Count: {count}</button>;
}
// startTransition for non-urgent updates
import { startTransition } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleSearch = (newQuery) => {
setQuery(newQuery); // Urgent update
startTransition(() => {
// Non-urgent update (can be interrupted)
setResults(searchData(newQuery));
});
};
return (
<div>
<input onChange={(e) => handleSearch(e.target.value)} />
<SearchResultsList results={results} />
</div>
);
}
// useDeferredValue for expensive computations
import { useDeferredValue } from 'react';
function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// Expensive computation uses deferred value
const results = useMemo(() => {
return searchData(deferredQuery);
}, [deferredQuery]);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<SearchResults results={results} />
</div>
);
}
8. Optimize Images and Assets#
Problem: Large images slow page load.
Solution: Optimize images and implement lazy loading.
// Lazy loading images
import { useState, useRef, useEffect } from 'react';
function LazyImage({ src, alt, placeholder }) {
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}>
{isInView && (
<img
src={src}
alt={alt}
onLoad={() => setIsLoaded(true)}
style={{ opacity: isLoaded ? 1 : 0 }}
/>
)}
{!isLoaded && <div>{placeholder}</div>}
</div>
);
}
// Modern image formats
function OptimizedImage({ src, alt }) {
return (
<picture>
<source srcSet={`${src}.webp`} type="image/webp" />
<source srcSet={`${src}.avif`} type="image/avif" />
<img src={`${src}.jpg`} alt={alt} loading="lazy" />
</picture>
);
}
// Responsive images
function ResponsiveImage({ src, alt }) {
return (
<img
src={src}
alt={alt}
srcSet={`
${src}-400w.jpg 400w,
${src}-800w.jpg 800w,
${src}-1200w.jpg 1200w
`}
sizes="(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px"
loading="lazy"
/>
);
}
9. Implement Proper Error Boundaries#
Problem: JavaScript errors crash entire app.
Solution: Use error boundaries to contain errors.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
// Log to error reporting service
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong.</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary>
<Header />
<ErrorBoundary>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary>
<MainContent />
</ErrorBoundary>
</ErrorBoundary>
);
}
// React 18 Error Boundary Hook (custom)
function useErrorBoundary() {
const [error, setError] = useState(null);
const resetError = () => setError(null);
const captureError = (error) => {
setError(error);
};
useEffect(() => {
if (error) {
throw error;
}
}, [error]);
return { captureError, resetError };
}
10. Optimize Bundle Size#
Problem: Large JavaScript bundles slow initial load.
Solution: Analyze and reduce bundle size.
// Tree shaking - import only what you need
// Bad
import * as _ from 'lodash';
const result = _.debounce(fn, 300);
// Good
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);
// Dynamic imports for large libraries
// Bad
import moment from 'moment';
// Good
const formatDate = async (date) => {
const moment = await import('moment');
return moment.default(date).format('YYYY-MM-DD');
};
// Webpack bundle splitting
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
},
},
},
},
};
// Remove unused code
// Use webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
npx webpack-bundle-analyzer build/static/js/*.js
11. Implement Service Workers#
Problem: Network requests slow app performance.
Solution: Cache resources with service workers.
// Register service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered: ', registration);
})
.catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}
// sw.js - Service worker
const CACHE_NAME = 'my-app-v1';
const urlsToCache = [
'/',
'/static/js/bundle.js',
'/static/css/main.css',
'/manifest.json'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Return cached version or fetch from network
return response || fetch(event.request);
})
);
});
// Workbox for advanced caching
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate, CacheFirst } from 'workbox-strategies';
cleanupOutdatedCaches();
precacheAndRoute(self.__WB_MANIFEST);
// Cache API responses
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new StaleWhileRevalidate({
cacheName: 'api-cache',
})
);
// Cache images
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
})
);
12. Use Web Workers for Heavy Computations#
Problem: Heavy computations block the main thread.
Solution: Move computations to web workers.
// worker.js
self.onmessage = function(e) {
const { data, operation } = e.data;
let result;
switch (operation) {
case 'sort':
result = data.sort((a, b) => a - b);
break;
case 'filter':
result = data.filter(item => item > 100);
break;
case 'calculate':
result = heavyCalculation(data);
break;
default:
result = data;
}
self.postMessage(result);
};
function heavyCalculation(data) {
// Expensive computation
let result = 0;
for (let i = 0; i < data.length; i++) {
result += Math.sqrt(data[i]) * Math.random();
}
return result;
}
// React component using web worker
function DataProcessor({ data }) {
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
const workerRef = useRef();
useEffect(() => {
workerRef.current = new Worker('/worker.js');
workerRef.current.onmessage = (e) => {
setResult(e.data);
setLoading(false);
};
return () => workerRef.current.terminate();
}, []);
const processData = (operation) => {
setLoading(true);
workerRef.current.postMessage({ data, operation });
};
return (
<div>
<button onClick={() => processData('sort')}>Sort Data</button>
<button onClick={() => processData('filter')}>Filter Data</button>
{loading && <div>Processing...</div>}
{result && <div>Result: {JSON.stringify(result.slice(0, 10))}</div>}
</div>
);
}
13. Optimize Context Usage#
Problem: Context updates cause unnecessary re-renders.
Solution: Split contexts and optimize providers.
// Bad: Single large context
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
const value = {
user, setUser,
theme, setTheme,
notifications, setNotifications
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
// Good: Split contexts
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(() => ({ user, setUser }), [user]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
// Optimize context selectors
function useUserSelector(selector) {
const context = useContext(UserContext);
return useMemo(() => selector(context), [context, selector]);
}
// Usage
function UserProfile() {
const userName = useUserSelector(context => context.user?.name);
return <div>{userName}</div>;
}
14. Implement Proper Loading States#
Problem: Poor loading experience hurts perceived performance.
Solution: Implement skeleton screens and progressive loading.
// Skeleton loading
function PostSkeleton() {
return (
<div className="animate-pulse">
<div className="h-4 bg-gray-300 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-300 rounded w-1/2 mb-2"></div>
<div className="h-32 bg-gray-300 rounded mb-2"></div>
<div className="h-4 bg-gray-300 rounded w-1/4"></div>
</div>
);
}
// Progressive loading
function PostList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchPosts()
.then(setPosts)
.catch(setError)
.finally(() => setLoading(false));
}, []);
if (error) return <ErrorMessage error={error} />;
return (
<div>
{posts.map(post => <PostCard key={post.id} post={post} />)}
{loading && Array(5).fill().map((_, i) => <PostSkeleton key={i} />)}
</div>
);
}
// Suspense boundaries
function App() {
return (
<div>
<Header />
<Suspense fallback={<PostSkeleton />}>
<PostList />
</Suspense>
<Suspense fallback={<div>Loading sidebar...</div>}>
<Sidebar />
</Suspense>
</div>
);
}
15. Monitor and Debug Performance#
Problem: Performance issues go unnoticed.
Solution: Implement monitoring and debugging tools.
// Performance monitoring
function usePerformanceMonitor(componentName) {
useEffect(() => {
const startTime = performance.now();
return () => {
const endTime = performance.now();
console.log(`${componentName} render time: ${endTime - startTime}ms`);
};
});
}
// Custom performance hook
function useRenderCount(componentName) {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
console.log(`${componentName} rendered ${renderCount.current} times`);
});
return renderCount.current;
}
// Usage
function MyComponent() {
usePerformanceMonitor('MyComponent');
const renderCount = useRenderCount('MyComponent');
return <div>Render count: {renderCount}</div>;
}
// Web Vitals monitoring
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);
// React DevTools Profiler API
import { Profiler } from 'react';
function onRenderCallback(id, phase, actualDuration, baseDuration, startTime, commitTime) {
// Log performance data
if (actualDuration > 16) { // Slower than 60fps
console.warn(`Slow render detected in ${id}: ${actualDuration}ms`);
}
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<MyApp />
</Profiler>
);
}
Performance Testing Strategy#
How to Test Performance#
Step 1: Establish Baseline
# Lighthouse audit
npm install -g lighthouse
lighthouse https://your-app.com --output html --output-path ./report.html
# Bundle size analysis
npm run build
npx webpack-bundle-analyzer build/static/js/*.js
Step 2: Load Testing
// Simple load test
function simulateUsers(count) {
for (let i = 0; i < count; i++) {
setTimeout(() => {
// Simulate user interactions
document.querySelector('button').click();
}, i * 100);
}
}
// Performance budget
const performanceBudget = {
maxBundleSize: 250000, // 250KB
maxRenderTime: 16, // 60fps
maxMemoryUsage: 50000000 // 50MB
};
Step 3: Continuous Monitoring
// Performance observer
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'measure') {
console.log(`${entry.name}: ${entry.duration}ms`);
}
}
});
observer.observe({ entryTypes: ['measure'] });
// Mark and measure
performance.mark('component-start');
// Component rendering
performance.mark('component-end');
performance.measure('component-render', 'component-start', 'component-end');
Common Performance Anti-Patterns#
What NOT to Do#
1. Inline object/function creation:
// Bad
function MyComponent() {
return <Child style={{ margin: 10 }} onClick={() => console.log('clicked')} />;
}
// Good
const style = { margin: 10 };
function MyComponent() {
const handleClick = useCallback(() => console.log('clicked'), []);
return <Child style={style} onClick={handleClick} />;
}
2. Unnecessary useEffect dependencies:
// Bad
useEffect(() => {
fetchData();
}, [user]); // Runs on every user property change
// Good
useEffect(() => {
fetchData();
}, [user.id]); // Only runs when user ID changes
3. Not cleaning up subscriptions:
// Bad
useEffect(() => {
const subscription = subscribe();
// Missing cleanup
}, []);
// Good
useEffect(() => {
const subscription = subscribe();
return () => subscription.unsubscribe();
}, []);
Frequently Asked Questions#
Q: When should I start optimizing React performance? A: Start optimizing when you notice performance issues or when your app grows beyond 50 components. Don't optimize prematurely, but do measure performance regularly. Use React DevTools Profiler to identify actual bottlenecks rather than guessing.
Q: Is React.memo always beneficial? A: No. React.memo adds overhead for the comparison check. Only use it for components that re-render frequently with the same props, or components with expensive rendering. For simple components that always receive different props, React.memo can actually hurt performance.
Q: How do I know if my optimizations are working? A: Use React DevTools Profiler to measure render times before and after optimization. Monitor Web Vitals (LCP, FID, CLS) and use Lighthouse audits. Set up performance budgets and track metrics over time. Real user monitoring is more valuable than synthetic tests.
Q: Should I use useMemo and useCallback everywhere? A: No. These hooks have overhead and should only be used when you have a proven performance problem. Use them for expensive calculations, stable references for memoized components, or when you've identified unnecessary re-renders through profiling.
Q: What's the biggest performance killer in React apps? A: Unnecessary re-renders are the most common issue. This usually happens due to: creating new objects/functions in render, not using proper keys in lists, context updates affecting too many components, or not memoizing expensive computations.
Q: How do I optimize a React app with thousands of list items? A: Use virtualization libraries like react-window or react-virtualized. Only render visible items plus a small buffer. Implement proper memoization for list items and use stable keys. Consider pagination or infinite scrolling for better UX.
Q: When should I use code splitting? A: Split code at route boundaries first, then for large features or rarely-used components. Split third-party libraries that are only used in specific parts of your app. Aim for initial bundle sizes under 250KB and individual chunks under 100KB.
Q: How do I handle performance in large React applications? A: Use a layered approach: optimize at the component level (memoization), application level (code splitting, lazy loading), and infrastructure level (CDN, caching). Implement performance monitoring and set up alerts for regressions.
Conclusion#
React performance optimization is about making smart trade-offs. Focus on the techniques that provide the biggest impact for your specific use case:
High Impact:
- Code splitting and lazy loading
- Proper list keys and virtualization
- Image optimization and lazy loading
- Bundle size reduction
Medium Impact:
- React.memo for expensive components
- useMemo/useCallback for proven bottlenecks
- Context optimization
- Service workers for caching
Low Impact (but still valuable):
- Error boundaries
- Performance monitoring
- Loading states
- Web workers for heavy computations
Your action plan:
- Measure current performance with React DevTools
- Implement code splitting for routes
- Optimize images and assets
- Add React.memo to expensive components
- Set up performance monitoring
- Create performance budgets
Remember: measure first, optimize second. Not all optimizations are worth the complexity they add.
Further Reading:
- React Performance Documentation - Official React performance guide
- Web Vitals - Google's performance metrics
- Learn more about our editorial team and how we research our articles.
Continue Reading
WebAssembly vs JavaScript: The Performance Revolution in 2026
WebAssembly is reshaping web development. Learn when and how to use WASM for maximum performance gains, with practical examples and real-world case studies.
Next.js Performance Optimization: 10 Essential Techniques
Essential Next.js performance optimization techniques. Learn image optimization, caching, bundle splitting, and how to improve Core Web Vitals.
Progressive Web Apps (PWA): The Complete 2026 Guide
Learn how to build Progressive Web Apps that work offline, load instantly, and feel like native apps. Includes service workers, caching strategies, and push notifications.
Browse by Topic
Find stories that matter to you.