Frontend Profiling¶
Measure and optimize frontend performance.
Core Web Vitals¶
| Metric | Good | Needs Work | Poor |
|---|---|---|---|
| LCP (Largest Contentful Paint) | < 2.5s | 2.5-4s | > 4s |
| FID (First Input Delay) | < 100ms | 100-300ms | > 300ms |
| CLS (Cumulative Layout Shift) | < 0.1 | 0.1-0.25 | > 0.25 |
| INP (Interaction to Next Paint) | < 200ms | 200-500ms | > 500ms |
Lighthouse¶
Chrome DevTools¶
- Open DevTools (F12)
- Go to Lighthouse tab
- Select categories and device
- Click "Analyze page load"
CLI¶
# Install
npm install -g lighthouse
# Run audit
lighthouse https://example.com --output html --output-path report.html
# CI-friendly JSON
lighthouse https://example.com --output json --chrome-flags="--headless"
Performance Budget¶
// lighthouserc.js
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000/'],
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'total-blocking-time': ['error', { maxNumericValue: 300 }],
},
},
},
};
React DevTools Profiler¶
Setup¶
- Install React DevTools browser extension
- Open DevTools → Profiler tab
- Click Record, interact with app, Stop
Reading Results¶
- Flamegraph: Time per component
- Ranked: Components sorted by render time
- Component chart: Renders over time
Highlight Updates¶
Settings → Highlight updates when components render
Shows which components re-render and why.
Performance Tab¶
Recording¶
- Open DevTools → Performance tab
- Click Record
- Interact with page
- Stop recording
Key Metrics¶
- Main Thread: JavaScript execution
- FPS: Should stay at 60
- Layout Shifts: CLS events
- Long Tasks: > 50ms tasks (red)
Finding Issues¶
Long task (200ms)
├── Event: click
├── Function Call: handleClick
│ ├── Function Call: expensiveCalculation (150ms) ← Bottleneck
│ └── Layout
└── Paint
Bundle Analysis¶
source-map-explorer¶
webpack-bundle-analyzer¶
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// config
});
Network Analysis¶
Waterfall Chart¶
DevTools → Network tab
Look for: - Blocking requests - Large resources - Many requests - Slow TTFB
Request Breakdown¶
| Phase | What It Means |
|---|---|
| Queueing | Waiting for connection |
| Stalled | Blocked by other requests |
| DNS Lookup | Resolving domain |
| Initial Connection | TCP handshake |
| SSL | TLS negotiation |
| TTFB | Server processing |
| Content Download | Data transfer |
Memory Profiling¶
Taking Heap Snapshots¶
- DevTools → Memory tab
- Select "Heap snapshot"
- Take snapshot
- Use app
- Take another snapshot
- Compare for leaks
Finding Leaks¶
Objects allocated between Snapshot 1 and Snapshot 2
├── (array) +5000 (could be a leak)
├── HTMLDivElement +100 (DOM nodes not cleaned up)
└── EventListener +50 (missing cleanup)
React-Specific Profiling¶
Why Did You Render¶
// src/wdyr.js
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
React.Profiler API¶
import { Profiler, ProfilerOnRenderCallback } from 'react';
const onRender: ProfilerOnRenderCallback = (
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) => {
console.log(`${id} ${phase}: ${actualDuration.toFixed(2)}ms`);
};
function App() {
return (
<Profiler id="App" onRender={onRender}>
<MainContent />
</Profiler>
);
}
Common Optimizations¶
Component Memoization¶
// Memoize expensive components
const MemoizedList = memo(ExpensiveList);
// Memoize callbacks
const handleClick = useCallback(() => {
// ...
}, [dependencies]);
// Memoize computed values
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
Code Splitting¶
// Route-based splitting
const Dashboard = lazy(() => import('./Dashboard'));
// Component-based splitting
const HeavyChart = lazy(() => import('./HeavyChart'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
);
}
Image Optimization¶
// Next.js Image
import Image from 'next/image';
<Image
src="/hero.jpg"
width={1200}
height={600}
priority // For LCP images
placeholder="blur"
/>
Virtual Lists¶
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: virtualItem.start,
height: virtualItem.size,
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
);
}
Performance Monitoring¶
Web Vitals Library¶
import { onCLS, onFID, onLCP, onINP } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
});
navigator.sendBeacon('/analytics', body);
}
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);
onINP(sendToAnalytics);