Building Performant React Applications in 2024
A comprehensive guide to optimising React applications for maximum performance, covering the latest techniques and best practices.
Introduction
React performance optimisation has evolved significantly over the years. With the introduction of Concurrent React, Server Components, and new patterns emerging in the ecosystem, there are more tools and techniques available than ever before to build lightning-fast applications.
In this comprehensive guide, we’ll explore the most effective strategies for optimising React applications in 2024, backed by real-world examples and measurable results.
Table of Contents
- Understanding React Performance
- Code Splitting and Lazy Loading
- State Management Optimisation
- Memoization Strategies
- Server-Side Rendering
- Measuring and Monitoring
Understanding React Performance
Before diving into optimisation techniques, it’s crucial to understand what affects React performance:
The React Render Cycle
React’s render cycle consists of two main phases:
- Render Phase: React calls your components to determine what should be on screen
- Commit Phase: React applies changes to the DOM
// Example: Understanding when re-renders occur
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// This effect runs on every render if not optimised
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // Dependency array prevents unnecessary re-runs
return <div>{user?.name}</div>;
}
Common Performance Bottlenecks
- Unnecessary Re-renders: Components rendering when their output hasn’t changed
- Large Bundle Sizes: Loading too much JavaScript upfront
- Inefficient Algorithms: O(n²) operations in render methods
- Memory Leaks: Uncleared timers and event listeners
Code Splitting and Lazy Loading
Modern bundlers make code splitting straightforward, but knowing when and how to split is crucial.
Route-Based Splitting
import { lazy, Suspense } from 'react';
import { Route, Routes } from 'react-router-dom';
// Lazy load route components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Component-Based Splitting
import { lazy, Suspense, useState } from 'react';
// Lazy load heavy components
const DataVisualization = lazy(() => import('./DataVisualization'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<h1>Dashboard</h1>
<button onClick={() => setShowChart(true)}>
Show Analytics
</button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<DataVisualization />
</Suspense>
)}
</div>
);
}
State Management Optimisation
Efficient state management is crucial for performance, especially in complex applications.
Context Optimisation
// ❌ Bad: Single context with all state
const AppContext = createContext({
user: null,
theme: 'light',
notifications: [],
// ... many other pieces of state
});
// ✅ Good: Separate contexts by concern
const UserContext = createContext(null);
const ThemeContext = createContext('light');
const NotificationContext = createContext([]);
// ✅ Even better: Optimise with useMemo
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(() => ({ user, setUser }), [user]);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
Reducing Prop Drilling
// Instead of passing props through multiple levels
function App() {
const [theme, setTheme] = useState('light');
return (
<Layout theme={theme} setTheme={setTheme}>
<Sidebar theme={theme} setTheme={setTheme}>
<ThemeToggle theme={theme} setTheme={setTheme} />
</Sidebar>
</Layout>
);
}
// Use context or state management library
function App() {
return (
<ThemeProvider>
<Layout>
<Sidebar>
<ThemeToggle />
</Sidebar>
</Layout>
</ThemeProvider>
);
}
Memoization Strategies
React provides several hooks for memoization, but knowing when to use them is key.
When to Use React.memo
// ✅ Good candidate: Expensive component with stable props
const ExpensiveChart = React.memo(({ data, options }) => {
const processedData = useMemo(() => {
return complexDataProcessing(data);
}, [data]);
return <Chart data={processedData} options={options} />;
});
// ❌ Poor candidate: Simple component that changes frequently
const SimpleCounter = React.memo(({ count }) => {
return <div>{count}</div>; // Changes on every increment
});
useMemo vs useCallback
function ProductList({ products, onProductClick }) {
// ✅ useMemo for expensive calculations
const sortedProducts = useMemo(() => {
return products.sort((a, b) => b.rating - a.rating);
}, [products]);
// ✅ useCallback for event handlers passed to memoized children
const handleProductClick = useCallback((productId) => {
onProductClick(productId);
}, [onProductClick]);
return (
<div>
{sortedProducts.map(product => (
<MemoizedProductCard
key={product.id}
product={product}
onClick={handleProductClick}
/>
))}
</div>
);
}
Server-Side Rendering
SSR can dramatically improve initial page load times and SEO.
Next.js App Router Example
// app/products/[id]/page.tsx
interface Props {
params: { id: string };
}
export default async function ProductPage({ params }: Props) {
// This runs on the server
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<ProductDetails product={product} />
</div>
);
}
// Generate static params for common products
export async function generateStaticParams() {
const popularProducts = await getPopularProducts();
return popularProducts.map((product) => ({
id: product.id,
}));
}
Streaming with Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>Product Page</h1>
{/* This renders immediately */}
<ProductInfo />
{/* This streams in when ready */}
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations />
</Suspense>
</div>
);
}
Measuring and Monitoring
Performance optimisation without measurement is guesswork.
React DevTools Profiler
// Wrap components you want to profile
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Router>
<Routes>
{/* Your routes */}
</Routes>
</Router>
</Profiler>
);
}
function onRenderCallback(id, phase, actualDuration) {
// Log performance data
console.log({ id, phase, actualDuration });
}
Web Vitals Monitoring
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
function sendToAnalytics({ name, value, id }) {
// Send to your analytics service
gtag('event', name, {
value: Math.round(name === 'CLS' ? value * 1000 : value),
event_label: id,
});
}
// Measure all Web Vitals
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
Performance Budgets
Setting and enforcing performance budgets helps maintain good performance over time.
// webpack.config.js
module.exports = {
performance: {
maxAssetSize: 250000, // 250kb
maxEntrypointSize: 400000, // 400kb
hints: 'error'
},
// Bundle analyzer
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: process.env.ANALYZE ? 'server' : 'disabled'
})
]
};
Conclusion
Building performant React applications requires a systematic approach:
- Measure First: Use tools like React DevTools Profiler and Web Vitals
- Optimise Strategically: Focus on the biggest impact optimisations first
- Monitor Continuously: Set up performance monitoring and budgets
- Stay Updated: Keep up with React’s evolving performance features
Remember, premature optimisation can be counterproductive. Always profile your application to identify real bottlenecks before optimising.
The techniques covered in this guide should give you a solid foundation for building fast, responsive React applications that provide excellent user experiences.
Further Reading:
Related Articles
The Evolution of CSS: From Floats to Container Queries
A journey through CSS evolution, exploring how layout techniques have advanced from table layouts to modern container queries and what's coming next.