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-rendersuseMemo 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.