React Performance Optimization
Master react performance optimization for building high-performance, scalable React applications with industry best practices.
React.memo and Component Memoization
React.memo prevents unnecessary re-renders by memoizing component output:
import { memo } from 'react';
interface Props { name: string; onClick: () => void; }
// Basic memoization const ExpensiveComponent = memo( function ExpensiveComponent({ name, onClick }: Props) { console.log('Rendering ExpensiveComponent'); return <button onClick={onClick}>{name}</button>; });
// Custom comparison function const CustomMemo = memo( function Component({ user }: { user: User }) { return <div>{user.name}</div>; }, (prevProps, nextProps) => { // Return true if passing nextProps would return the same result as prevProps return prevProps.user.id === nextProps.user.id; } );
// When to use custom comparison const ProductCard = memo( function ProductCard({ product }: { product: Product }) { return ( <div> <h3>{product.name}</h3> <p>${product.price}</p> </div> ); }, (prev, next) => { // Only re-render if these specific fields change return ( prev.product.id === next.product.id && prev.product.name === next.product.name && prev.product.price === next.product.price ); } );
useMemo for Expensive Computations
import { useMemo, useState } from 'react';
function DataTable({ items }: { items: Item[] }) { const [filter, setFilter] = useState(''); const [sortBy, setSortBy] = useState<'name' | 'price'>('name');
// Expensive filtering and sorting const processedItems = useMemo(() => { console.log('Computing filtered and sorted items'); return items .filter(item => item.name.toLowerCase().includes(filter.toLowerCase())) .sort((a, b) => { if (sortBy === 'name') { return a.name.localeCompare(b.name); } return a.price - b.price; }); }, [items, filter, sortBy]);
// Expensive aggregate calculation const statistics = useMemo(() => { console.log('Computing statistics'); return { total: processedItems.reduce((sum, item) => sum + item.price, 0), average: processedItems.length ? processedItems.reduce((sum, item) => sum + item.price, 0) / processedItems.length : 0, count: processedItems.length }; }, [processedItems]);
return ( <> <input value={filter} onChange={(e) => setFilter(e.target.value)} /> <select value={sortBy} onChange={(e) => setSortBy(e.target.value as any)}> <option value="name">Name</option> <option value="price">Price</option> </select> <div>Total: ${statistics.total}</div> <div>Average: ${statistics.average.toFixed(2)}</div> <div>Count: {statistics.count}</div> {processedItems.map(item => ( <div key={item.id}>{item.name} - ${item.price}</div> ))} </> ); }
useCallback for Stable Function References
import { useCallback, useState, memo } from 'react';
// Child component that only re-renders when necessary const ListItem = memo(function ListItem({ item, onDelete }: { item: Item; onDelete: (id: string) => void; }) { console.log('Rendering ListItem', item.id); return ( <div> {item.name} <button onClick={() => onDelete(item.id)}>Delete</button> </div> ); });
function OptimizedList({ items }: { items: Item[] }) { const [deletedIds, setDeletedIds] = useState<Set<string>>(new Set());
// Without useCallback, this creates a new function on every render // causing ListItem to re-render even with memo const handleDelete = useCallback((id: string) => { setDeletedIds(prev => new Set([...prev, id])); // API call to delete api.deleteItem(id); }, []); // Empty deps means function never changes
const handleDeleteWithDeps = useCallback((id: string) => { console.log('Already deleted:', deletedIds.size); setDeletedIds(prev => new Set([...prev, id])); }, [deletedIds]); // Re-create when deletedIds changes
const visibleItems = items.filter(item => !deletedIds.has(item.id));
return ( <> {visibleItems.map(item => ( <ListItem key={item.id} item={item} onDelete={handleDelete} /> ))} </> ); }
Code Splitting with React.lazy and Suspense
import { lazy, Suspense } from 'react'; import { Routes, Route } 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')); const Analytics = lazy(() => import('./pages/Analytics'));
// Fallback component function LoadingSpinner() { return <div className="spinner">Loading...</div>; }
function App() { return ( <Suspense fallback={<LoadingSpinner />}> <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/profile" element={<Profile />} /> <Route path="/settings" element={<Settings />} /> <Route path="/analytics" element={<Analytics />} /> </Routes> </Suspense> ); }
// Preload on hover for better UX function Navigation() { const preloadDashboard = () => import('./pages/Dashboard'); const preloadProfile = () => import('./pages/Profile');
return ( <nav> <a href="/dashboard" onMouseEnter={preloadDashboard}>Dashboard</a> <a href="/profile" onMouseEnter={preloadProfile}>Profile</a> </nav> ); }
// Nested Suspense boundaries function DashboardLayout() { const Header = lazy(() => import('./components/Header')); const Sidebar = lazy(() => import('./components/Sidebar')); const Content = lazy(() => import('./components/Content'));
return ( <div className="dashboard"> <Suspense fallback={<div>Loading header...</div>}> <Header /> </Suspense> <Suspense fallback={<div>Loading sidebar...</div>}> <Sidebar /> </Suspense> <Suspense fallback={<div>Loading content...</div>}> <Content /> </Suspense> </div> ); }
Virtual Scrolling for Large Lists
import { FixedSizeList, VariableSizeList } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer';
// Fixed size items function VirtualList({ items }: { items: string[] }) { const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => ( <div style={style} className="list-item"> {items[index]} </div> );
return ( <FixedSizeList height={600} itemCount={items.length} itemSize={35} width="100%" > {Row} </FixedSizeList> ); }
// Variable size items function VariableList({ items }: { items: Post[] }) { const getItemSize = (index: number) => { // Calculate height based on content return items[index].content.length > 100 ? 120 : 80; };
const Row = ({ index, style }: any) => ( <div style={style} className="post"> <h3>{items[index].title}</h3> <p>{items[index].content}</p> </div> );
return ( <VariableSizeList height={600} itemCount={items.length} itemSize={getItemSize} width="100%" > {Row} </VariableSizeList> ); }
// With AutoSizer for responsive layouts function ResponsiveList({ items }: { items: Item[] }) { return ( <div style={{ height: '100vh', width: '100%' }}> <AutoSizer> {({ height, width }) => ( <FixedSizeList height={height} itemCount={items.length} itemSize={50} width={width} > {({ index, style }) => ( <div style={style}>{items[index].name}</div> )} </FixedSizeList> )} </AutoSizer> </div> ); }
React Profiler API for Performance Monitoring
import { Profiler, ProfilerOnRenderCallback } from 'react';
const onRenderCallback: ProfilerOnRenderCallback = (
id, // the "id" prop of the Profiler tree that has just committed
phase, // either "mount" (first render) or "update" (re-render)
actualDuration, // time spent rendering the committed update
baseDuration, // estimated time to render the entire subtree without memoization
startTime, // when React began rendering this update
commitTime, // when React committed this update
interactions // the Set of interactions belonging to this update
) => {
console.log(${id} (${phase}) took ${actualDuration}ms);
// Send to analytics if (actualDuration > 100) { analytics.track('slow-render', { component: id, duration: actualDuration, phase }); } };
function App() { return ( <Profiler id="App" onRender={onRenderCallback}> <Dashboard /> </Profiler> ); }
// Nested profilers for granular monitoring function Dashboard() { return ( <div> <Profiler id="Sidebar" onRender={onRenderCallback}> <Sidebar /> </Profiler> <Profiler id="Content" onRender={onRenderCallback}> <Content /> </Profiler> </div> ); }
Bundle Size Optimization
// Use dynamic imports for large libraries function ChartComponent() { const [Chart, setChart] = useState<any>(null);
useEffect(() => { // Only load chart library when needed import('chart.js').then(module => { setChart(() => module.Chart); }); }, []);
if (!Chart) return <div>Loading chart...</div>;
return <Chart data={data} />; }
// Tree-shakeable imports // GOOD: Import only what you need import { format } from 'date-fns';
// BAD: Imports entire library import moment from 'moment';
// Use webpack magic comments for chunk names const AdminPanel = lazy(() => import(/* webpackChunkName: "admin" */ './AdminPanel') );
const UserDashboard = lazy(() => import(/* webpackChunkName: "dashboard" */ './UserDashboard') );
Image Optimization Techniques
import { useState, useEffect } from 'react';
// Lazy loading images function LazyImage({ src, alt, placeholder }: { src: string; alt: string; placeholder?: string; }) { const [imageSrc, setImageSrc] = useState(placeholder || ''); const [imageRef, setImageRef] = useState<HTMLImageElement | null>(null);
useEffect(() => { if (!imageRef) return;
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setImageSrc(src);
observer.unobserve(imageRef);
}
});
});
observer.observe(imageRef);
return () => {
if (imageRef) observer.unobserve(imageRef);
};
}, [imageRef, src]);
return ( <img ref={setImageRef} src={imageSrc} alt={alt} loading="lazy" /> ); }
// Progressive image loading function ProgressiveImage({ src, placeholder }: { src: string; placeholder: string; }) { const [currentSrc, setCurrentSrc] = useState(placeholder); const [loading, setLoading] = useState(true);
useEffect(() => { const img = new Image(); img.src = src; img.onload = () => { setCurrentSrc(src); setLoading(false); }; }, [src]);
return ( <img src={currentSrc} style={{ filter: loading ? 'blur(10px)' : 'none', transition: 'filter 0.3s' }} /> ); }
Concurrent Features: useTransition and useDeferredValue
import { useState, useTransition, useDeferredValue } from 'react';
// useTransition for non-urgent updates function SearchResults() { const [query, setQuery] = useState(''); const [results, setResults] = useState<Result[]>([]); const [isPending, startTransition] = useTransition();
const handleSearch = (value: string) => { setQuery(value); // Urgent: update input immediately
// Non-urgent: defer expensive search
startTransition(() => {
const searchResults = performExpensiveSearch(value);
setResults(searchResults);
});
};
return ( <> <input value={query} onChange={e => handleSearch(e.target.value)} placeholder="Search..." /> {isPending && <div>Searching...</div>} <ResultsList results={results} /> </> ); }
// useDeferredValue for deferring expensive renders function ProductList({ products }: { products: Product[] }) { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query);
// This filters with the deferred value // UI stays responsive while filtering const filteredProducts = useMemo(() => { return products.filter(p => p.name.toLowerCase().includes(deferredQuery.toLowerCase()) ); }, [products, deferredQuery]);
return ( <> <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Filter products..." /> {query !== deferredQuery && <div>Updating...</div>} <div> {filteredProducts.map(product => ( <ProductCard key={product.id} product={product} /> ))} </div> </> ); }
Debouncing and Throttling
import { useState, useEffect, useCallback, useRef } from 'react';
// Custom debounce hook function useDebounce<T>(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue; }
// Usage with search function SearchWithDebounce() { const [query, setQuery] = useState(''); const debouncedQuery = useDebounce(query, 500);
useEffect(() => { if (debouncedQuery) { // Only search after user stops typing for 500ms performSearch(debouncedQuery); } }, [debouncedQuery]);
return ( <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search..." /> ); }
// Custom throttle hook function useThrottle<T>(value: T, limit: number): T { const [throttledValue, setThrottledValue] = useState<T>(value); const lastRan = useRef(Date.now());
useEffect(() => { const handler = setTimeout(() => { if (Date.now() - lastRan.current >= limit) { setThrottledValue(value); lastRan.current = Date.now(); } }, limit - (Date.now() - lastRan.current));
return () => clearTimeout(handler);
}, [value, limit]);
return throttledValue; }
// Throttle scroll events function InfiniteScroll() { const [scrollPosition, setScrollPosition] = useState(0); const throttledScroll = useThrottle(scrollPosition, 200);
useEffect(() => { const handleScroll = () => setScrollPosition(window.scrollY); window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, []);
useEffect(() => { // Only process scroll every 200ms console.log('Throttled scroll position:', throttledScroll); }, [throttledScroll]);
return <div>Scroll position: {throttledScroll}</div>; }
Optimizing Context Performance
import { createContext, useContext, useState, useMemo, ReactNode } from 'react';
// Split context to prevent unnecessary re-renders const StateContext = createContext<State | null>(null); const DispatchContext = createContext<Dispatch | null>(null);
function Provider({ children }: { children: ReactNode }) { const [state, setState] = useState<State>(initialState);
// Memoize dispatch to keep it stable const dispatch = useMemo( () => ({ updateUser: (user: User) => setState(s => ({ ...s, user })), updateSettings: (settings: Settings) => setState(s => ({ ...s, settings })) }), [] );
return ( <StateContext.Provider value={state}> <DispatchContext.Provider value={dispatch}> {children} </DispatchContext.Provider> </StateContext.Provider> ); }
// Components only re-render when they use state that actually changes function UserProfile() { const state = useContext(StateContext); // Re-renders on any state change return <div>{state?.user.name}</div>; }
function SettingsButton() { const dispatch = useContext(DispatchContext); // Never re-renders return <button onClick={() => dispatch?.updateSettings({})}>Settings</button>; }
Web Workers for Heavy Computations
import { useEffect, useState } from 'react';
// worker.ts // self.addEventListener('message', (e) => { // const result = performHeavyCalculation(e.data); // self.postMessage(result); // });
function useWebWorker<T, R>(workerFn: (data: T) => R) { const [result, setResult] = useState<R | null>(null); const [error, setError] = useState<Error | null>(null); const [loading, setLoading] = useState(false);
const execute = (data: T) => { setLoading(true); setError(null);
const worker = new Worker(
URL.createObjectURL(
new Blob([`(${workerFn.toString()})()`], { type: 'application/javascript' })
)
);
worker.postMessage(data);
worker.onmessage = (e) => {
setResult(e.data);
setLoading(false);
worker.terminate();
};
worker.onerror = (e) => {
setError(new Error(e.message));
setLoading(false);
worker.terminate();
};
};
return { result, error, loading, execute }; }
// Usage function DataProcessor() { const { result, loading, execute } = useWebWorker( (data: number[]) => { // Heavy calculation runs in worker return data.reduce((sum, n) => sum + n * n, 0); } );
const handleProcess = () => { execute(Array.from({ length: 1000000 }, (_, i) => i)); };
return ( <div> <button onClick={handleProcess} disabled={loading}> Process Data </button> {loading && <div>Processing...</div>} {result && <div>Result: {result}</div>} </div> ); }
When to Use This Skill
Use react-performance when you need to:
-
Optimize slow-rendering components
-
Reduce bundle size with code splitting
-
Handle large lists with virtualization
-
Prevent unnecessary re-renders
-
Improve application load time
-
Optimize expensive computations
-
Build performant React applications
-
Debug performance issues
-
Implement lazy loading strategies
-
Improve Core Web Vitals scores
-
Optimize for mobile devices
-
Handle real-time data efficiently
Best Practices
Profile before optimizing - Use React DevTools Profiler to identify actual bottlenecks before applying optimizations.
Use React.memo wisely - Only memoize components that render often with the same props or have expensive render logic.
Memoize callbacks and values - Use useCallback for functions passed to memoized children, useMemo for expensive computations.
Code split by route - Lazy load route components to reduce initial bundle size and improve load time.
Virtualize long lists - Use react-window or react-virtualized for lists with more than 100 items.
Optimize images - Lazy load images, use appropriate formats (WebP), implement progressive loading.
Debounce expensive operations - Debounce search inputs, API calls, and other expensive operations.
Split context strategically - Separate read and write contexts to prevent unnecessary consumer re-renders.
Monitor bundle size - Use webpack-bundle-analyzer to identify and remove large dependencies.
Use concurrent features - Leverage useTransition and useDeferredValue for better perceived performance.
Common Pitfalls
Premature optimization - Don't optimize without measuring. Profile first, then optimize bottlenecks.
Overusing memo - Memoizing everything adds overhead. Only memoize when there's a measurable benefit.
Wrong dependencies - Missing dependencies in useMemo/useCallback leads to stale closures and bugs.
Not measuring impact - Always measure performance improvements with React Profiler or browser tools.
Ignoring bundle size - Importing large libraries for small features significantly impacts load time.
Memoizing primitives - useMemo is unnecessary for primitive values or simple calculations.
Not using key prop - Missing or incorrect keys in lists cause unnecessary re-renders and bugs.
Inline function definitions - Creating functions inline in JSX prevents React.memo from working effectively.
Not code splitting - Loading entire app upfront increases initial load time dramatically.
Forgetting about network - Optimize data fetching, use pagination, implement proper caching strategies.
Resources
-
React Documentation - Performance
-
React DevTools Profiler
-
Web.dev - React Performance
-
React Performance Optimization Tips
-
Bundle Size Optimization
-
react-window Documentation