React Performance Optimization Techniques
React Performance Optimization Techniques
Optimizing React applications is crucial for providing a smooth user experience. This guide covers practical techniques to identify and fix performance issues in your React applications.
Measuring Performance
Before optimizing, you need to measure performance to identify bottlenecks:
React DevTools Profiler
The React DevTools Profiler is your first line of defense:
- Install React DevTools browser extension
- Open DevTools, navigate to the "Profiler" tab
- Click the record button, interact with your app, then stop recording
- Analyze component render times and commit frequency
Lighthouse and Web Vitals
Use Lighthouse in Chrome DevTools to measure overall performance metrics like:
- First Contentful Paint (FCP)
- Largest Contentful Paint (LCP)
- Time to Interactive (TTI)
- Cumulative Layout Shift (CLS)
Component Optimization
Prevent Unnecessary Renders with React.memo
React.memo
is a higher-order component that memoizes your component, preventing re-renders if props haven't changed:
const MovieCard = React.memo(function MovieCard({ title, poster, rating }) {
return (
<div className="movie-card">
<img src={poster} alt={title} />
<h3>{title}</h3>
<div>Rating: {rating}/10</div>
</div>
);
});
Use it for components that:
- Render often
- Re-render with the same props
- Are medium to large in complexity
Custom Comparison Function
You can provide a custom comparison function to React.memo
for more control:
function arePropsEqual(prevProps, nextProps) {
// Only re-render if the rating changed by more than 0.5
return (
prevProps.title === nextProps.title &&
Math.abs(prevProps.rating - nextProps.rating) <= 0.5
);
}
const MovieCard = React.memo(MovieCardComponent, arePropsEqual);
useMemo for Expensive Calculations
Use useMemo
to cache the results of expensive calculations:
function MovieList({ movies, filter }) {
// This calculation only runs when movies or filter changes
const filteredMovies = useMemo(() => {
console.log('Filtering movies...');
return movies.filter(movie => {
return movie.title.toLowerCase().includes(filter.toLowerCase());
});
}, [movies, filter]);
return (
<div>
{filteredMovies.map(movie => (
<MovieCard key={movie.id} {...movie} />
))}
</div>
);
}
useCallback for Stable Function References
Use useCallback
to prevent function recreation on each render:
function MovieList({ movies }) {
const [favorites, setFavorites] = useState([]);
// This function reference remains stable between renders
const handleFavorite = useCallback((movieId) => {
setFavorites(prev => {
if (prev.includes(movieId)) {
return prev.filter(id => id !== movieId);
} else {
return [...prev, movieId];
}
});
}, []);
return (
<div>
{movies.map(movie => (
<MovieCard
key={movie.id}
{...movie}
isFavorite={favorites.includes(movie.id)}
onFavorite={handleFavorite} // Stable reference
/>
))}
</div>
);
}
State Management Optimization
State Colocation
Keep state as close as possible to where it's used:
// Bad: State is too high in the tree
function App() {
const [searchTerm, setSearchTerm] = useState('');
// This causes the entire app to re-render when typing
return (
<div>
<Header />
<SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
<MovieList />
<Footer />
</div>
);
}
// Good: State is colocated with its usage
function App() {
return (
<div>
<Header />
<SearchFeature /> {/* Contains both SearchBar and filtered results */}
<Footer />
</div>
);
}
function SearchFeature() {
const [searchTerm, setSearchTerm] = useState('');
return (
<div>
<SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
<FilteredMovieList searchTerm={searchTerm} />
</div>
);
}
State Normalization
For complex state, normalize your data like a database:
// Before: Nested state that's hard to update
const [state, setState] = useState({
users: [
{ id: 1, name: 'John', posts: [{ id: 1, title: 'Hello' }] }
]
});
// After: Normalized state
const [state, setState] = useState({
users: {
1: { id: 1, name: 'John', postIds: [1] }
},
posts: {
1: { id: 1, title: 'Hello', userId: 1 }
}
});
Rendering Optimization
Virtualization for Long Lists
Use a virtualization library like react-window
or react-virtualized
for long lists:
import { FixedSizeList } from 'react-window';
function MovieList({ movies }) {
const Row = ({ index, style }) => (
<div style={style}>
<MovieCard {...movies[index]} />
</div>
);
return (
<FixedSizeList
height={500}
width="100%"
itemCount={movies.length}
itemSize={150}
>
{Row}
</FixedSizeList>
);
}
Code Splitting
Use dynamic imports to split your code into smaller chunks:
import React, { Suspense, lazy } from 'react';
// Instead of: import MovieDetails from './MovieDetails';
const MovieDetails = lazy(() => import('./MovieDetails'));
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<MovieList />} />
<Route path="/movie/:id" element={<MovieDetails />} />
</Routes>
</Suspense>
</div>
);
}
Avoid Inline Function Definitions
Inline functions create new function instances on every render:
// Bad: New function created on every render
function MovieList({ movies }) {
return (
<div>
{movies.map(movie => (
<MovieCard
key={movie.id}
{...movie}
onFavorite={() => handleFavorite(movie.id)} // New function every time
/>
))}
</div>
);
}
// Good: Stable function with useCallback
function MovieList({ movies }) {
const handleFavorite = useCallback((movieId) => {
// Handle favorite logic
}, []);
return (
<div>
{movies.map(movie => (
<MovieCard
key={movie.id}
{...movie}
movieId={movie.id} // Pass ID as prop
onFavorite={handleFavorite} // Pass stable function
/>
))}
</div>
);
}
Advanced Techniques
Lazy Loading Images
Implement lazy loading for images to improve initial load time:
import { useEffect, useRef, useState } from 'react';
function LazyImage({ src, alt, ...props }) {
const [isLoaded, setIsLoaded] = useState(false);
const imgRef = useRef();
useEffect(() => {
if (!imgRef.current) return;
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setIsLoaded(true);
observer.disconnect();
}
});
});
observer.observe(imgRef.current);
return () => {
observer.disconnect();
};
}, []);
return (
<div ref={imgRef}>
{isLoaded ? (
<img src={src} alt={alt} {...props} />
) : (
<div className="placeholder" />
)}
</div>
);
}
Web Workers for CPU-Intensive Tasks
Move CPU-intensive operations to a web worker:
// worker.js
self.addEventListener('message', e => {
const { data, filters } = e.data;
// Expensive data processing
const processed = data.filter(item => {
// Complex filtering logic
return complexFilterLogic(item, filters);
});
self.postMessage(processed);
});
// Component
function DataProcessor({ data, filters }) {
const [processed, setProcessed] = useState([]);
useEffect(() => {
const worker = new Worker('./worker.js');
worker.onmessage = e => {
setProcessed(e.data);
};
worker.postMessage({ data, filters });
return () => worker.terminate();
}, [data, filters]);
return (
<div>
{processed.map(item => (
<Item key={item.id} {...item} />
))}
</div>
);
}
Conclusion
Performance optimization in React is an ongoing process. Start by measuring to identify real bottlenecks, then apply the appropriate techniques to address them. Remember that premature optimization can lead to unnecessary complexity, so focus on optimizing components that are causing actual performance issues.
By applying these techniques strategically, you can significantly improve the performance and user experience of your React applications.