React Performance Optimization - 2025 Guide
React performance optimization requires understanding when and how components re-render, effective use of memoization, and proper code splitting strategies.
1. Component Memoization
React.memo Usage
// Memoize expensive components
const ExpensiveComponent = React.memo(function ExpensiveComponent({
data,
onAction
}: {
data: DataType[];
onAction: (id: string) => void;
}) {
const processedData = useMemo(() => {
return data.map(item => ({
...item,
computed: expensiveComputation(item)
}));
}, [data]);
return (
<div>
{processedData.map(item => (
<ExpensiveItem
key={item.id}
item={item}
onAction={onAction}
/>
))}
</div>
);
});
// Memoize item components
const ExpensiveItem = React.memo(function ExpensiveItem({
item,
onAction
}: {
item: ProcessedDataType;
onAction: (id: string) => void;
}) {
const handleClick = useCallback(() => {
onAction(item.id);
}, [item.id, onAction]);
return (
<div onClick={handleClick}>
{item.name}: {item.computed}
</div>
);
});
useMemo and useCallback
function DataVisualization({ data, filters }: Props) {
// Memoize expensive calculations
const filteredData = useMemo(() => {
return data.filter(item =>
filters.every(filter => filter.predicate(item))
);
}, [data, filters]);
const chartData = useMemo(() => {
return processChartData(filteredData);
}, [filteredData]);
// Memoize event handlers
const handleFilterChange = useCallback((newFilters: Filter[]) => {
setFilters(newFilters);
}, []);
const handleDataExport = useCallback(async () => {
const exportData = await processExportData(filteredData);
downloadFile(exportData);
}, [filteredData]);
return (
<div>
<FilterPanel onFilterChange={handleFilterChange} />
<Chart data={chartData} />
<ExportButton onExport={handleDataExport} />
</div>
);
}
2. Code Splitting and Lazy Loading
Component Lazy Loading
// Lazy load heavy components
const LazyDataTable = lazy(() => import('./DataTable'));
const LazyChart = lazy(() => import('./Chart'));
const LazyModal = lazy(() => import('./Modal'));
function Dashboard() {
const [activeTab, setActiveTab] = useState('overview');
const [showModal, setShowModal] = useState(false);
return (
<div>
<TabNavigation activeTab={activeTab} onTabChange={setActiveTab} />
<Suspense fallback={<TableSkeleton />}>
{activeTab === 'data' && <LazyDataTable />}
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
{activeTab === 'analytics' && <LazyChart />}
</Suspense>
{showModal && (
<Suspense fallback={<ModalSkeleton />}>
<LazyModal onClose={() => setShowModal(false)} />
</Suspense>
)}
</div>
);
}
Route-Based Code Splitting
// Router with lazy loading
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const HomePage = lazy(() => import('./pages/HomePage'));
const DashboardPage = lazy(() => import('./pages/DashboardPage'));
const AnalyticsPage = lazy(() => import('./pages/AnalyticsPage'));
const SettingsPage = lazy(() => import('./pages/SettingsPage'));
function App() {
return (
<Routes>
<Route
path="/"
element={
<Suspense fallback={<PageSkeleton />}>
<HomePage />
</Suspense>
}
/>
<Route
path="/dashboard"
element={
<Suspense fallback={<PageSkeleton />}>
<DashboardPage />
</Suspense>
}
/>
<Route
path="/analytics"
element={
<Suspense fallback={<PageSkeleton />}>
<AnalyticsPage />
</Suspense>
}
/>
</Routes>
);
}
3. Virtual Scrolling for Large Lists
import { FixedSizeList as List } from 'react-window';
interface VirtualizedListProps {
items: ListItem[];
onItemClick: (item: ListItem) => void;
}
function VirtualizedList({ items, onItemClick }: VirtualizedListProps) {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => {
const item = items[index];
return (
<div
style={style}
onClick={() => onItemClick(item)}
className="flex items-center p-4 border-b hover:bg-gray-50"
>
<img src={item.avatar} alt="" className="w-10 h-10 rounded-full mr-3" />
<div>
<div className="font-medium">{item.name}</div>
<div className="text-sm text-gray-500">{item.email}</div>
</div>
</div>
);
};
return (
<List
height={400}
itemCount={items.length}
itemSize={80}
overscanCount={5}
>
{Row}
</List>
);
}
4. Image Optimization
// Lazy loading images with intersection observer
function LazyImage({
src,
alt,
className,
placeholder = '/placeholder.jpg'
}: {
src: string;
alt: string;
className?: string;
placeholder?: string;
}) {
const [imageSrc, setImageSrc] = useState(placeholder);
const [imageRef, setImageRef] = useState<HTMLImageElement | null>(null);
useEffect(() => {
let observer: IntersectionObserver;
if (imageRef && imageSrc === placeholder) {
observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setImageSrc(src);
observer.unobserve(imageRef);
}
});
},
{ threshold: 0.1 }
);
observer.observe(imageRef);
}
return () => {
if (observer && imageRef) {
observer.unobserve(imageRef);
}
};
}, [imageRef, imageSrc, placeholder, src]);
return (
<img
ref={setImageRef}
src={imageSrc}
alt={alt}
className={className}
loading="lazy"
/>
);
}
// Progressive image loading
function ProgressiveImage({ src, placeholder, alt, className }: {
src: string;
placeholder: string;
alt: string;
className?: string;
}) {
const [imageSrc, setImageSrc] = useState(placeholder);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const img = new Image();
img.src = src;
img.onload = () => {
setImageSrc(src);
setLoaded(true);
};
}, [src]);
return (
<div className={`relative overflow-hidden ${className}`}>
<img
src={imageSrc}
alt={alt}
className={`transition-opacity duration-300 ${
loaded ? 'opacity-100' : 'opacity-70'
}`}
/>
{!loaded && (
<div className="absolute inset-0 bg-gray-200 animate-pulse" />
)}
</div>
);
}
5. State Management Optimization
// Split context to prevent unnecessary re-renders
const ThemeContext = createContext();
const UserContext = createContext();
const DataContext = createContext();
// Use selectors with Zustand
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
const useAppStore = create(
subscribeWithSelector((set, get) => ({
user: null,
theme: 'light',
data: [],
filters: [],
// Actions
setUser: (user) => set({ user }),
setTheme: (theme) => set({ theme }),
setData: (data) => set({ data }),
setFilters: (filters) => set({ filters }),
}))
);
// Optimized selectors
function UserProfile() {
// Only re-renders when user changes
const user = useAppStore((state) => state.user);
return <div>{user?.name}</div>;
}
function ThemeToggle() {
// Only re-renders when theme changes
const theme = useAppStore((state) => state.theme);
const setTheme = useAppStore((state) => state.setTheme);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
{theme}
</button>
);
}
6. Performance Monitoring
// Performance monitoring hook
function usePerformanceMonitoring() {
useEffect(() => {
// Monitor Core Web Vitals
if ('web-vital' in window) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(console.log);
getFID(console.log);
getFCP(console.log);
getLCP(console.log);
getTTFB(console.log);
});
}
// Monitor long tasks
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn('Long task detected:', entry);
}
}
});
observer.observe({ entryTypes: ['longtask'] });
return () => observer.disconnect();
}
}, []);
}
// React Profiler wrapper
function ProfiledComponent({ children, id }: {
children: React.ReactNode;
id: string;
}) {
const onRenderCallback = (
id: string,
phase: "mount" | "update",
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number
) => {
if (actualDuration > 16) { // More than one frame
console.warn(`Slow render in ${id}:`, {
phase,
actualDuration,
baseDuration
});
}
};
return (
<Profiler id={id} onRender={onRenderCallback}>
{children}
</Profiler>
);
}
7. Bundle Optimization
// Vite configuration for optimization
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
react(),
visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true,
}),
],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
ui: ['@mui/material', '@emotion/react'],
charts: ['recharts', 'd3'],
},
},
},
chunkSizeWarningLimit: 500,
},
optimizeDeps: {
include: ['react', 'react-dom', 'react-router-dom'],
},
});
8. Memory Leak Prevention
// Cleanup subscriptions and timers
function useTimer() {
const [time, setTime] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setTime(prev => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return time;
}
// Cleanup event listeners
function useWindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const updateSize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', updateSize);
updateSize();
return () => window.removeEventListener('resize', updateSize);
}, []);
return size;
}
// Cleanup async operations
function useAsyncOperation() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
setLoading(true);
try {
const result = await api.getData();
if (!cancelled) {
setData(result);
}
} catch (error) {
if (!cancelled) {
console.error(error);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, []);
return { data, loading };
}
Performance Checklist
Rendering Optimization
- [ ] Use React.memo for expensive components
- [ ] Implement useMemo for expensive calculations
- [ ] Use useCallback for event handlers passed to children
- [ ] Avoid creating objects/functions in render
- [ ] Use key props correctly for lists
Code Splitting
- [ ] Implement route-based code splitting
- [ ] Lazy load heavy components
- [ ] Split vendor bundles appropriately
- [ ] Use dynamic imports for conditional features
State Management
- [ ] Split context providers by concern
- [ ] Use state selectors to prevent unnecessary re-renders
- [ ] Avoid storing derived state
- [ ] Implement proper state normalization
Assets and Images
- [ ] Implement lazy loading for images
- [ ] Use appropriate image formats (WebP, AVIF)
- [ ] Implement image compression
- [ ] Use CDN for static assets
Monitoring
- [ ] Monitor Core Web Vitals
- [ ] Set up performance budgets
- [ ] Use React DevTools Profiler
- [ ] Track bundle size over time