React Performance Optimization
Learn how to optimize your React applications for better performance and user experience.
Why Performance Matters
Fast applications provide better user experience, improve conversion rates, and rank higher in search results. Even small improvements in load time and responsiveness can significantly impact user satisfaction.
Measuring React Performance
React DevTools Profiler
Install React DevTools browser extension and use the Profiler tab:
- Open your app with React DevTools
- Switch to the Profiler tab
- Click “Record” and interact with your app
- Stop recording and analyze the results
Performance Monitoring
// Using React Profiler API
import { Profiler } from 'react';
function onRenderCallback(id, phase, actualDuration, baseDuration, startTime, commitTime) {
console.log('Component render timing:', {
id,
phase,
actualDuration,
baseDuration
});
}
function MyComponent() {
return (
<Profiler id="MyComponent" onRender={onRenderCallback}>
<ExpensiveComponent />
</Profiler>
);
}Common Performance Issues
Unnecessary Re-renders
React re-renders components when:
- Props change
- State changes
- Parent component re-renders
- Context values change
Heavy Computations
Expensive calculations running on every render can slow down your app.
Large Bundle Sizes
Too much JavaScript code increases load times and affects performance.
Optimization Techniques
1. React.memo for Component Memoization
Prevent unnecessary re-renders of functional components:
import React from 'react';
// Without memoization - re-renders when parent re-renders
const UserCard = ({ user }) => {
console.log('UserCard rendered');
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
};
// With memoization - only re-renders when props change
const UserCard = React.memo(({ user }) => {
console.log('UserCard rendered');
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
});
// Custom comparison function
const UserCard = React.memo(({ user }) => {
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}, (prevProps, nextProps) => {
// Only re-render if user ID changed
return prevProps.user.id === nextProps.user.id;
});2. useMemo for Expensive Calculations
Cache expensive computations:
import React, { useMemo } from 'react';
function ExpensiveCalculation({ numbers }) {
// Without useMemo - recalculates on every render
const sum = numbers.reduce((acc, num) => acc + num, 0);
// With useMemo - only recalculates when numbers change
const sum = useMemo(() => {
console.log('Calculating sum...');
return numbers.reduce((acc, num) => acc + num, 0);
}, [numbers]);
// Complex example
const processedData = useMemo(() => {
return numbers
.filter(num => num > 0)
.map(num => num * 2)
.sort((a, b) => a - b);
}, [numbers]);
return <div>Sum: {sum}</div>;
}3. useCallback for Function References
Prevent function recreation that causes child re-renders:
import React, { useState, useCallback } from 'react';
// Without useCallback - new function on every render
const ParentComponent = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return <ChildComponent onClick={handleClick} />;
};
// With useCallback - same function reference
const ParentComponent = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []); // Empty dependency array
// With dependencies
const handleIncrement = useCallback((amount) => {
setCount(prevCount => prevCount + amount);
}, []); // Still empty since we use functional update
return <ChildComponent onClick={handleClick} />;
};
const ChildComponent = React.memo(({ onClick }) => {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Click me</button>;
});4. Code Splitting with React.lazy
Load components only when needed:
import React, { Suspense, lazy } from 'react';
// Regular import - loads immediately
import HeavyComponent from './HeavyComponent';
// Lazy loading - loads when rendered
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
const [showHeavy, setShowHeavy] = useState(false);
return (
<div>
<button onClick={() => setShowHeavy(true)}>
Load Heavy Component
</button>
{showHeavy && (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
)}
</div>
);
}
// Route-based code splitting
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 App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}5. Virtualization for Long Lists
Use react-window or react-virtualized for large lists:
import { FixedSizeList as List } from 'react-window';
// Regular approach - renders all items
const RegularList = ({ items }) => (
<div>
{items.map(item => (
<div key={item.id} style={{ height: 50 }}>
{item.name}
</div>
))}
</div>
);
// Virtualized approach - renders only visible items
const VirtualizedList = ({ items }) => {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<List
height={400}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</List>
);
};6. State Management Optimization
Use Local State When Possible
// Bad - global state for local UI
function FormComponent() {
const [globalState, setGlobalState] useGlobalState();
return (
<form>
<input
value={globalState.inputValue}
onChange={(e) => setGlobalState({ inputValue: e.target.value })}
/>
</form>
);
}
// Good - local state for UI concerns
function FormComponent() {
const [inputValue, setInputValue] = useState('');
const handleSubmit = () => {
// Only update global state when needed
updateGlobalState({ inputValue });
};
return (
<form onSubmit={handleSubmit}>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
</form>
);
}Context Optimization
// Bad - single context causes re-renders for all consumers
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 concern
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = 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 }}>
<NotificationContext.Provider value={{ notifications, setNotifications }}>
{children}
</NotificationContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}7. Image Optimization
// 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} className="image-container">
{isInView && (
<img
src={src}
alt={alt}
onLoad={() => setIsLoaded(true)}
style={{ opacity: isLoaded ? 1 : 0 }}
/>
)}
{!isLoaded && <div className="placeholder">{placeholder}</div>}
</div>
);
}Bundle Optimization
1. Tree Shaking
Ensure your bundler can remove unused code:
// Bad - imports entire library
import * as _ from 'lodash';
// Good - imports specific functions
import { debounce, throttle } from 'lodash';
// Even better - use smaller alternatives
import { debounce } from 'lodash-es';2. Dynamic Imports
Load libraries only when needed:
// Regular import - adds to main bundle
import moment from 'moment';
// Dynamic import - creates separate chunk
const loadMoment = async () => {
const moment = await import('moment');
return moment.default;
};
// Usage
const handleDateClick = async () => {
const moment = await loadMoment();
const formatted = moment().format('MMMM Do YYYY');
console.log(formatted);
};Performance Monitoring in Production
1. Web Vitals
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);2. Error Boundaries
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Log to monitoring service
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}Performance Checklist
Component Level
- Use React.memo for pure components
- Wrap expensive calculations in useMemo
- Use useCallback for stable function references
- Avoid inline object definitions in JSX
- Split large components into smaller ones
Application Level
- Implement code splitting with React.lazy
- Use virtualization for long lists
- Optimize bundle size with tree shaking
- Implement lazy loading for images
- Use appropriate state management
Monitoring
- Set up performance monitoring
- Track Core Web Vitals
- Implement error boundaries
- Monitor bundle size over time
- Test on real devices
Tools for Performance Optimization
Development Tools
- React DevTools Profiler: Identify performance bottlenecks
- Chrome DevTools: Analyze runtime performance
- Bundle Analyzer: Visualize bundle composition
Build Tools
- Webpack Bundle Analyzer: Analyze bundle size
- Create React App: Built-in optimizations
- Next.js: Automatic code splitting and optimization
Monitoring Services
- Sentry: Error tracking and performance monitoring
- LogRocket: Session replay and performance insights
- New Relic: Application performance monitoring
Common Performance Pitfalls
1. Over-optimization
Don’t optimize prematurely. Measure first, then optimize.
2. Incorrect Dependencies
Wrong dependency arrays in useMemo/useCallback can cause bugs:
// Bad - missing dependency
const filteredData = useMemo(() => {
return data.filter(item => item.category === selectedCategory);
}, []); // Missing selectedCategory
// Good - correct dependencies
const filteredData = useMemo(() => {
return data.filter(item => item.category === selectedCategory);
}, [data, selectedCategory]);3. Excessive Memoization
Not everything needs to be memoized. Use it judiciously.
Resources
- React Performance Documentation
- Web.dev Performance
- React DevTools Profiler Guide
- Bundle Size Optimization
Next Steps
Start by measuring your app’s current performance, identify the biggest bottlenecks, and apply the most impactful optimizations first. Remember that performance is an ongoing process, not a one-time fix.