Comprehensive guide to optimizing React applications for maximum performance, covering rendering optimization, bundle splitting, and advanced patterns.
React applications often suffer from performance issues including unnecessary re-renders, large bundle sizes, and poor user experience.
Comprehensive performance optimization strategy covering rendering optimization, bundle splitting, and advanced React patterns.
Significant improvement in application performance, reduced bundle sizes, and enhanced user experience.
React performance optimization is crucial for delivering smooth user experiences, especially in large-scale applications. This comprehensive guide covers advanced techniques to maximize React application performance.
React's rendering process involves several phases:
Common performance issues in React applications:
import React, { memo } from 'react'
interface UserCardProps {
user: User
onEdit: (id: string) => void
}
const UserCard = memo<UserCardProps>(({ user, onEdit }) => {
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={() => onEdit(user.id)}>Edit</button>
</div>
)
}, (prevProps, nextProps) => {
// Custom comparison function
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name &&
prevProps.user.email === nextProps.user.email
)
})
export default UserCard
import React, { useMemo } from 'react'
interface DataVisualizationProps {
data: DataPoint[]
filters: FilterOptions
}
const DataVisualization: React.FC<DataVisualizationProps> = ({ data, filters }) => {
const processedData = useMemo(() => {
console.log('Processing data...') // Only runs when dependencies change
return data
.filter(item => item.category === filters.category)
.map(item => ({
...item,
normalizedValue: item.value / item.maxValue,
trend: calculateTrend(item.historicalData)
}))
.sort((a, b) => b.normalizedValue - a.normalizedValue)
}, [data, filters.category]) // Dependencies array
return (
<div className="visualization">
{processedData.map(item => (
<DataPoint key={item.id} data={item} />
))}
</div>
)
}
import React, { useCallback, useState } from 'react'
interface TodoListProps {
todos: Todo[]
}
const TodoList: React.FC<TodoListProps> = ({ todos }) => {
const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all')
// Memoized callback to prevent child re-renders
const handleToggleTodo = useCallback((id: string) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
))
}, [])
const handleDeleteTodo = useCallback((id: string) => {
setTodos(prev => prev.filter(todo => todo.id !== id))
}, [])
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed)
case 'completed':
return todos.filter(todo => todo.completed)
default:
return todos
}
}, [todos, filter])
return (
<div>
<FilterButtons filter={filter} onFilterChange={setFilter} />
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggleTodo}
onDelete={handleDeleteTodo}
/>
))}
</div>
)
}
import React, { useMemo, useRef, useState, useEffect } from 'react'
interface VirtualListProps<T> {
items: T[]
itemHeight: number
containerHeight: number
renderItem: (item: T, index: number) => React.ReactNode
}
function VirtualList<T>({
items,
itemHeight,
containerHeight,
renderItem
}: VirtualListProps<T>) {
const [scrollTop, setScrollTop] = useState(0)
const containerRef = useRef<HTMLDivElement>(null)
const visibleItems = useMemo(() => {
const startIndex = Math.floor(scrollTop / itemHeight)
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / itemHeight) + 1,
items.length
)
return items.slice(startIndex, endIndex).map((item, index) => ({
item,
index: startIndex + index
}))
}, [items, scrollTop, itemHeight, containerHeight])
const totalHeight = items.length * itemHeight
const offsetY = Math.floor(scrollTop / itemHeight) * itemHeight
return (
<div
ref={containerRef}
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map(({ item, index }) => (
<div
key={index}
style={{ height: itemHeight }}
>
{renderItem(item, index)}
</div>
))}
</div>
</div>
</div>
)
}
// Usage
const LargeDataList: React.FC<{ data: DataPoint[] }> = ({ data }) => {
return (
<VirtualList
items={data}
itemHeight={60}
containerHeight={400}
renderItem={(item, index) => (
<div className="data-item">
<span>{item.name}</span>
<span>{item.value}</span>
</div>
)}
/>
)
}
import React, { Suspense, lazy } from 'react'
import { Routes, Route } from 'react-router-dom'
// Lazy load components
const Dashboard = lazy(() => import('./Dashboard'))
const Analytics = lazy(() => import('./Analytics'))
const Settings = lazy(() => import('./Settings'))
// Loading component
const LoadingSpinner = () => (
<div className="loading-spinner">
<div className="spinner" />
<p>Loading...</p>
</div>
)
// Route-based code splitting
const App: React.FC = () => {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
)
}
// Component-based code splitting
const LazyModal = lazy(() => import('./Modal'))
const ParentComponent: React.FC = () => {
const [showModal, setShowModal] = useState(false)
return (
<div>
<button onClick={() => setShowModal(true)}>Open Modal</button>
{showModal && (
<Suspense fallback={<div>Loading modal...</div>}>
<LazyModal onClose={() => setShowModal(false)} />
</Suspense>
)}
</div>
)
}
import React, { createContext, useContext, useMemo, useCallback } from 'react'
// Split contexts to prevent unnecessary re-renders
interface UserContextType {
user: User | null
login: (credentials: LoginCredentials) => Promise<void>
logout: () => void
}
interface ThemeContextType {
theme: 'light' | 'dark'
toggleTheme: () => void
}
const UserContext = createContext<UserContextType | null>(null)
const ThemeContext = createContext<ThemeContextType | null>(null)
// Optimized context provider
const UserProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(false)
const login = useCallback(async (credentials: LoginCredentials) => {
setLoading(true)
try {
const userData = await authService.login(credentials)
setUser(userData)
} finally {
setLoading(false)
}
}, [])
const logout = useCallback(() => {
setUser(null)
authService.logout()
}, [])
const value = useMemo(() => ({
user,
login,
logout
}), [user, login, logout])
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
)
}
// Custom hooks for context consumption
const useUser = () => {
const context = useContext(UserContext)
if (!context) {
throw new Error('useUser must be used within UserProvider')
}
return context
}
const useTheme = () => {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
// ... other config
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
],
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendors',
chunks: 'all',
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
enforce: true
}
}
}
}
}
// Dynamic import with webpack magic comments
const loadChartLibrary = () => import(
/* webpackChunkName: "chart-library" */
/* webpackPrefetch: true */
'chart.js'
)
const loadDateLibrary = () => import(
/* webpackChunkName: "date-library" */
'date-fns'
)
// Usage in component
const ChartComponent: React.FC = () => {
const [chartLib, setChartLib] = useState(null)
useEffect(() => {
loadChartLibrary().then(lib => {
setChartLib(lib)
})
}, [])
if (!chartLib) return <div>Loading chart...</div>
return <div>Chart component</div>
}
// ❌ Bad: Imports entire library
import _ from 'lodash'
// ✅ Good: Imports only needed functions
import { debounce, throttle } from 'lodash'
// ❌ Bad: Imports entire component library
import { Button, Input, Modal } from 'antd'
// ✅ Good: Imports from specific paths
import Button from 'antd/lib/button'
import Input from 'antd/lib/input'
import Modal from 'antd/lib/modal'
// ❌ Bad: Imports entire utility library
import * as utils from './utils'
// ✅ Good: Imports specific utilities
import { formatDate, validateEmail } from './utils'
import React, { useEffect, useRef } from 'react'
const DataVisualization: React.FC = () => {
const chartRef = useRef<HTMLCanvasElement>(null)
const chartInstanceRef = useRef<Chart | null>(null)
useEffect(() => {
if (chartRef.current) {
// Create chart instance
chartInstanceRef.current = new Chart(chartRef.current, {
type: 'line',
data: chartData,
options: chartOptions
})
}
// Cleanup function
return () => {
if (chartInstanceRef.current) {
chartInstanceRef.current.destroy()
chartInstanceRef.current = null
}
}
}, [])
return <canvas ref={chartRef} />
}
import React, { useEffect, useRef } from 'react'
const ScrollToTop: React.FC = () => {
const buttonRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
const handleScroll = () => {
if (window.scrollY > 300) {
buttonRef.current?.classList.add('visible')
} else {
buttonRef.current?.classList.remove('visible')
}
}
window.addEventListener('scroll', handleScroll, { passive: true })
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
return (
<button
ref={buttonRef}
onClick={scrollToTop}
className="scroll-to-top"
>
↑
</button>
)
}
import React, { Profiler } from 'react'
const onRenderCallback = (
id: string,
phase: 'mount' | 'update',
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number
) => {
console.log('Profiler:', {
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
})
}
const App: React.FC = () => {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Router>
</Profiler>
)
}
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'
const sendToAnalytics = (metric: any) => {
// Send to your analytics service
analytics.track('performance_metric', {
name: metric.name,
value: metric.value,
delta: metric.delta,
id: metric.id
})
}
// Measure Core Web Vitals
getCLS(sendToAnalytics)
getFID(sendToAnalytics)
getFCP(sendToAnalytics)
getLCP(sendToAnalytics)
getTTFB(sendToAnalytics)
interface DataProviderProps {
children: (data: any, loading: boolean, error: Error | null) => React.ReactNode
}
const DataProvider: React.FC<DataProviderProps> = ({ children }) => {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
fetchData()
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [])
return <>{children(data, loading, error)}</>
}
// Usage
const App: React.FC = () => {
return (
<DataProvider>
{(data, loading, error) => {
if (loading) return <LoadingSpinner />
if (error) return <ErrorMessage error={error} />
return <DataVisualization data={data} />
}}
</DataProvider>
)
}
import React, { ComponentType } from 'react'
interface WithLoadingProps {
loading: boolean
}
const withLoading = <P extends object>(
Component: ComponentType<P>
): ComponentType<P & WithLoadingProps> => {
return (props: P & WithLoadingProps) => {
const { loading, ...rest } = props
if (loading) {
return <LoadingSpinner />
}
return <Component {...(rest as P)} />
}
}
// Usage
const UserProfile = ({ user }: { user: User }) => (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
const UserProfileWithLoading = withLoading(UserProfile)
import { render, screen } from '@testing-library/react'
import { act } from 'react-dom/test-utils'
describe('Performance Tests', () => {
it('should render large list efficiently', () => {
const largeDataset = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
value: Math.random()
}))
const startTime = performance.now()
act(() => {
render(<VirtualList items={largeDataset} itemHeight={50} containerHeight={400} />)
})
const endTime = performance.now()
const renderTime = endTime - startTime
expect(renderTime).toBeLessThan(100) // Should render in less than 100ms
})
})
// bundle-size.test.js
const fs = require('fs')
const path = require('path')
describe('Bundle Size Tests', () => {
it('should not exceed maximum bundle size', () => {
const bundlePath = path.join(__dirname, '../dist/static/js/main.js')
const bundleSize = fs.statSync(bundlePath).size
const maxSize = 500 * 1024 // 500KB
expect(bundleSize).toBeLessThan(maxSize)
})
})
React performance optimization is a continuous process that requires understanding of React's rendering mechanism, careful profiling, and strategic application of optimization techniques. The key is to:
Remember that premature optimization can lead to complex, hard-to-maintain code. Always profile your application first to identify actual performance bottlenecks before applying optimization techniques.
This guide covers advanced React performance optimization techniques based on real-world experience and industry best practices.
Ashutosh Malve
AI Solution Architect
Published on January 5, 2024