Back to Blog
ReactPerformanceProfilingOptimization

React Performance Profiling Guide

Profile and optimize React applications. From React DevTools to performance patterns to measuring real-world performance.

B
Bootspring Team
Engineering
March 12, 2022
6 min read

Finding and fixing performance issues requires proper measurement. Here's how to profile React applications effectively.

React DevTools Profiler#

1// Enable profiling in development 2// React DevTools extension shows Profiler tab 3 4// Wrap specific components for profiling 5import { Profiler } from 'react'; 6 7function onRenderCallback( 8 id: string, 9 phase: 'mount' | 'update', 10 actualDuration: number, 11 baseDuration: number, 12 startTime: number, 13 commitTime: number 14) { 15 console.log({ 16 id, 17 phase, 18 actualDuration, // Time spent rendering 19 baseDuration, // Time without memoization 20 startTime, 21 commitTime, 22 }); 23} 24 25function App() { 26 return ( 27 <Profiler id="App" onRender={onRenderCallback}> 28 <Header /> 29 <MainContent /> 30 <Footer /> 31 </Profiler> 32 ); 33} 34 35// Profile specific expensive components 36function Dashboard() { 37 return ( 38 <div> 39 <Profiler id="Charts" onRender={onRenderCallback}> 40 <ExpensiveCharts /> 41 </Profiler> 42 <Profiler id="DataGrid" onRender={onRenderCallback}> 43 <DataGrid data={data} /> 44 </Profiler> 45 </div> 46 ); 47}

Chrome Performance Tab#

1// Mark performance events 2function expensiveOperation() { 3 performance.mark('operation-start'); 4 5 // ... expensive work 6 7 performance.mark('operation-end'); 8 performance.measure('operation', 'operation-start', 'operation-end'); 9} 10 11// Get measurements 12const measures = performance.getEntriesByName('operation'); 13console.log(measures[0].duration); 14 15// Clear marks 16performance.clearMarks(); 17performance.clearMeasures(); 18 19// User Timing API for components 20function DataTable({ data }: { data: Item[] }) { 21 useEffect(() => { 22 performance.mark('DataTable-render-start'); 23 24 return () => { 25 performance.mark('DataTable-render-end'); 26 performance.measure( 27 'DataTable-render', 28 'DataTable-render-start', 29 'DataTable-render-end' 30 ); 31 }; 32 }, []); 33 34 return <table>{/* ... */}</table>; 35}

Finding Re-render Causes#

1// Hook to track why component re-rendered 2function useWhyDidYouUpdate<T extends Record<string, any>>( 3 name: string, 4 props: T 5) { 6 const previousProps = useRef<T>(); 7 8 useEffect(() => { 9 if (previousProps.current) { 10 const allKeys = Object.keys({ ...previousProps.current, ...props }); 11 const changesObj: Record<string, { from: any; to: any }> = {}; 12 13 allKeys.forEach((key) => { 14 if (previousProps.current![key] !== props[key]) { 15 changesObj[key] = { 16 from: previousProps.current![key], 17 to: props[key], 18 }; 19 } 20 }); 21 22 if (Object.keys(changesObj).length) { 23 console.log('[why-did-you-update]', name, changesObj); 24 } 25 } 26 27 previousProps.current = props; 28 }); 29} 30 31// Usage 32function MyComponent(props: Props) { 33 useWhyDidYouUpdate('MyComponent', props); 34 return <div>{/* ... */}</div>; 35} 36 37// Track render count 38function useRenderCount(componentName: string) { 39 const renderCount = useRef(0); 40 renderCount.current++; 41 42 useEffect(() => { 43 console.log(`${componentName} render count: ${renderCount.current}`); 44 }); 45 46 return renderCount.current; 47}

Measuring Component Performance#

1// Performance observer hook 2function usePerformanceObserver(callback: (entries: PerformanceEntry[]) => void) { 3 useEffect(() => { 4 const observer = new PerformanceObserver((list) => { 5 callback(list.getEntries()); 6 }); 7 8 observer.observe({ entryTypes: ['measure', 'mark', 'longtask'] }); 9 10 return () => observer.disconnect(); 11 }, [callback]); 12} 13 14// Component timing HOC 15function withTiming<P extends object>( 16 WrappedComponent: React.ComponentType<P>, 17 name: string 18) { 19 return function TimedComponent(props: P) { 20 useEffect(() => { 21 const startTime = performance.now(); 22 23 return () => { 24 const duration = performance.now() - startTime; 25 console.log(`${name} mounted for ${duration}ms`); 26 }; 27 }, []); 28 29 const renderStart = performance.now(); 30 31 useEffect(() => { 32 const renderDuration = performance.now() - renderStart; 33 console.log(`${name} rendered in ${renderDuration}ms`); 34 }); 35 36 return <WrappedComponent {...props} />; 37 }; 38} 39 40// Usage 41const TimedExpensiveComponent = withTiming(ExpensiveComponent, 'ExpensiveComponent');

React.memo Optimization#

1// Basic memoization 2const MemoizedComponent = React.memo(function Component({ data }: Props) { 3 return <div>{data.name}</div>; 4}); 5 6// Custom comparison function 7const MemoizedList = React.memo( 8 function List({ items }: { items: Item[] }) { 9 return ( 10 <ul> 11 {items.map((item) => ( 12 <li key={item.id}>{item.name}</li> 13 ))} 14 </ul> 15 ); 16 }, 17 (prevProps, nextProps) => { 18 // Return true if props are equal (skip re-render) 19 if (prevProps.items.length !== nextProps.items.length) { 20 return false; 21 } 22 return prevProps.items.every( 23 (item, index) => item.id === nextProps.items[index].id 24 ); 25 } 26); 27 28// When NOT to use React.memo 29// - Component always receives different props 30// - Component renders quickly anyway 31// - Props are primitives and parent rarely re-renders

useMemo and useCallback#

1// Memoize expensive calculations 2function DataAnalytics({ data }: { data: DataPoint[] }) { 3 // Only recalculates when data changes 4 const statistics = useMemo(() => { 5 return { 6 average: data.reduce((a, b) => a + b.value, 0) / data.length, 7 max: Math.max(...data.map((d) => d.value)), 8 min: Math.min(...data.map((d) => d.value)), 9 sorted: [...data].sort((a, b) => b.value - a.value), 10 }; 11 }, [data]); 12 13 return <StatsDisplay stats={statistics} />; 14} 15 16// Memoize callbacks to prevent child re-renders 17function Parent() { 18 const [count, setCount] = useState(0); 19 const [name, setName] = useState(''); 20 21 // Without useCallback, new function created every render 22 const handleClick = useCallback(() => { 23 setCount((c) => c + 1); 24 }, []); 25 26 // Child only re-renders when handleClick changes (never) 27 return ( 28 <div> 29 <input value={name} onChange={(e) => setName(e.target.value)} /> 30 <MemoizedChild onClick={handleClick} /> 31 </div> 32 ); 33} 34 35// Know when to skip memoization 36// ❌ Unnecessary - inline handler is fine for simple cases 37<button onClick={() => setOpen(true)}>Open</button> 38 39// ✓ Useful - prevents MemoizedChild re-render 40<MemoizedChild onClick={useCallback(() => doSomething(), [])} />

Virtualization for Large Lists#

1import { FixedSizeList } from 'react-window'; 2 3function VirtualizedList({ items }: { items: Item[] }) { 4 const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => ( 5 <div style={style}> 6 <ListItem item={items[index]} /> 7 </div> 8 ); 9 10 return ( 11 <FixedSizeList 12 height={600} 13 width="100%" 14 itemCount={items.length} 15 itemSize={50} 16 > 17 {Row} 18 </FixedSizeList> 19 ); 20} 21 22// Variable size list 23import { VariableSizeList } from 'react-window'; 24 25function VariableVirtualizedList({ items }: { items: Item[] }) { 26 const getItemSize = (index: number) => { 27 return items[index].isExpanded ? 100 : 50; 28 }; 29 30 return ( 31 <VariableSizeList 32 height={600} 33 width="100%" 34 itemCount={items.length} 35 itemSize={getItemSize} 36 > 37 {({ index, style }) => ( 38 <div style={style}> 39 <ListItem item={items[index]} /> 40 </div> 41 )} 42 </VariableSizeList> 43 ); 44}

Code Splitting and Lazy Loading#

1import { lazy, Suspense } from 'react'; 2 3// Lazy load heavy components 4const HeavyChart = lazy(() => import('./HeavyChart')); 5const DataGrid = lazy(() => import('./DataGrid')); 6 7function Dashboard() { 8 return ( 9 <div> 10 <Suspense fallback={<ChartSkeleton />}> 11 <HeavyChart data={chartData} /> 12 </Suspense> 13 14 <Suspense fallback={<GridSkeleton />}> 15 <DataGrid rows={rows} /> 16 </Suspense> 17 </div> 18 ); 19} 20 21// Named exports need wrapper 22const Modal = lazy(() => 23 import('./Modal').then((module) => ({ default: module.Modal })) 24); 25 26// Preload on hover/focus 27const preloadComponent = () => { 28 import('./HeavyComponent'); 29}; 30 31<button 32 onMouseEnter={preloadComponent} 33 onFocus={preloadComponent} 34 onClick={() => setShowHeavy(true)} 35> 36 Show Component 37</button>

Web Vitals#

1// Measure Core Web Vitals 2import { getCLS, getFID, getLCP, getFCP, getTTFB } from 'web-vitals'; 3 4function reportWebVitals(metric: any) { 5 console.log(metric); 6 7 // Send to analytics 8 fetch('/api/analytics', { 9 method: 'POST', 10 body: JSON.stringify(metric), 11 }); 12} 13 14getCLS(reportWebVitals); // Cumulative Layout Shift 15getFID(reportWebVitals); // First Input Delay 16getLCP(reportWebVitals); // Largest Contentful Paint 17getFCP(reportWebVitals); // First Contentful Paint 18getTTFB(reportWebVitals); // Time to First Byte 19 20// Custom performance hook 21function useWebVitals() { 22 const [vitals, setVitals] = useState<Record<string, number>>({}); 23 24 useEffect(() => { 25 getCLS((metric) => setVitals((v) => ({ ...v, cls: metric.value }))); 26 getFID((metric) => setVitals((v) => ({ ...v, fid: metric.value }))); 27 getLCP((metric) => setVitals((v) => ({ ...v, lcp: metric.value }))); 28 }, []); 29 30 return vitals; 31}

Best Practices#

Profiling: ✓ Profile in production mode ✓ Test with realistic data ✓ Measure before optimizing ✓ Focus on user-perceived performance Optimization: ✓ Start with architecture (code splitting) ✓ Then component memoization ✓ Then virtualization if needed ✓ Avoid premature optimization Common Issues: ✓ Unnecessary re-renders ✓ Large bundle sizes ✓ Expensive calculations in render ✓ Memory leaks from subscriptions

Conclusion#

Performance profiling starts with measurement. Use React DevTools Profiler for component-level analysis, Chrome Performance tab for overall app performance, and Web Vitals for user-perceived metrics. Optimize based on data, not assumptions. Focus on the biggest bottlenecks first and always verify improvements with profiling.

Share this article

Help spread the word about Bootspring