Performance Optimization Techniques for React Apps
Performance is crucial for web applications. I've spent a lot of time optimizing React apps, and I've learned that it's not just about making things faster—it's about creating a smooth, responsive user experience. React provides many tools for optimization, but knowing when and how to use them is key.
Understanding performance bottlenecks
Before you start optimizing, you need to identify what's slow. I always start with measurement:
- Chrome DevTools Performance tab: Record and analyze runtime performance
- Lighthouse: Automated performance audits
- React DevTools Profiler: Component-level performance analysis
- Bundle analyzer: Identify large dependencies
Code splitting
Code splitting is one of the most impactful optimizations. Instead of loading everything at once, split your code into chunks that load when needed.
Route-based splitting with React.lazy:
import { lazy, Suspense } from 'react';
import { BrowserRouter as Router, 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 (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</Router>
);
}Component-based splitting:
import { lazy, Suspense } from 'react';
const HeavyChart = lazy(() => import('./components/HeavyChart'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>
Show Analytics
</button>
{showChart && (
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart />
</Suspense>
)}
</div>
);
}Memoization
Memoization prevents unnecessary re-renders by caching expensive computations.
React.memo for components:
const UserCard = React.memo(({ user, onClick }) => {
console.log('UserCard rendered for:', user.name);
return (
<div onClick={() => onClick(user.id)}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
});useMemo for expensive calculations:
function UserList({ users, filter }) {
const filteredUsers = useMemo(() => {
console.log('Filtering users...');
return users.filter(user =>
user.name.toLowerCase().includes(filter.toLowerCase())
);
}, [users, filter]);
return (
<div>
{filteredUsers.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}useCallback for stable function references:
function ParentComponent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // Empty dependency array since it doesn't depend on props/state
return (
<div>
<ChildComponent onClick={handleClick} />
<p>Count: {count}</p>
</div>
);
}Virtualization for large lists
When displaying hundreds or thousands of items, virtualization renders only what's visible.
Using react-window:
import { FixedSizeList as List } from 'react-window';
function VirtualizedList({ items }) {
return (
<List
height={400}
itemCount={items.length}
itemSize={50}
width={300}
>
{({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
)}
</List>
);
}Optimizing bundle size
Large bundles slow down initial page loads. Several strategies help:
Tree shaking:
// Only import what you need
import { map, filter } from 'lodash-es'; // Instead of import _ from 'lodash'Dynamic imports:
// Load heavy libraries only when needed
const handleClick = async () => {
const { heavyFunction } = await import('./heavy-utils');
heavyFunction();
};Bundle analysis:
npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
};Image optimization
Images often account for most of a page's weight.
Next.js Image component:
import Image from 'next/image';
export default function OptimizedImage() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={800}
height={600}
priority // For above-the-fold images
placeholder="blur"
sizes="(max-width: 768px) 100vw, 50vw"
/>
);
}Modern image formats:
<picture> <source srcset="image.avif" type="image/avif"> <source srcset="image.webp" type="image/webp"> <img src="image.jpg" alt="Modern image"> </picture>
State management optimization
Efficient state management prevents unnecessary renders.
Context with useMemo:
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const contextValue = useMemo(() => ({
theme,
setTheme,
}), [theme]);
return (
<ThemeContext.Provider value={contextValue}>
{children}
</ThemeContext.Provider>
);
}Redux optimization:
// Use reselect for computed values
import { createSelector } from 'reselect';
const getVisibleTodos = createSelector(
[getTodos, getFilter],
(todos, filter) => {
switch (filter) {
case 'SHOW_COMPLETED':
return todos.filter(todo => todo.completed);
case 'SHOW_ACTIVE':
return todos.filter(todo => !todo.completed);
default:
return todos;
}
}
);Server-side rendering and hydration
SSR can improve initial page load performance.
Next.js SSR:
// pages/ssr-page.js
export async function getServerSideProps() {
const data = await fetchData();
return { props: { data } };
}
export default function SSRPage({ data }) {
return (
<div>
<h1>Server-Side Rendered</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}Static generation for static content:
// pages/static-page.js
export async function getStaticProps() {
const data = await fetchStaticData();
return { props: { data } };
}
export default function StaticPage({ data }) {
return <div>{/* Static content */}</div>;
}Monitoring and measurement
Performance optimization is ongoing. Set up monitoring:
Web Vitals:
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
getCLS(console.log);
getFID(console.log);
getFCP(console.log);
getLCP(console.log);
getTTFB(console.log);Real user monitoring (RUM):
// Using a service like Sentry or LogRocket
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: 'your-dsn',
tracesSampleRate: 1.0,
});Common performance pitfalls
Avoid these common mistakes:
- Creating functions in render: Move them outside or use useCallback
- Object/array creation in render: Use useMemo
- Unnecessary re-renders: Check dependency arrays
- Large bundle sizes: Code split and lazy load
- Inefficient selectors: Use reselect for Redux
- Missing keys: Always provide keys for lists
Performance budgeting
Set performance budgets and monitor them:
// webpack.config.js
module.exports = {
performance: {
hints: 'warning',
maxAssetSize: 500000,
maxEntrypointSize: 500000,
},
};Continuous optimization
Performance optimization isn't a one-time task:
- Regular audits: Use Lighthouse regularly
- Monitor trends: Track performance over time
- User feedback: Listen to performance complaints
- A/B testing: Test performance improvements
- Stay updated: Keep up with React and browser updates
My optimization philosophy
I focus on perceived performance as much as actual performance. Users care about how fast things feel, not just technical metrics. Techniques like skeleton screens, optimistic updates, and progressive loading can make apps feel faster even if the raw numbers aren't perfect.
Start with measurement, identify bottlenecks, and optimize iteratively. The biggest performance gains usually come from the first few optimizations. After that, you're often dealing with diminishing returns.
Remember that performance is a feature. Investing in it early pays dividends in user satisfaction and business metrics. A fast app is a competitive advantage.
Related articles