|$ curl https://forge-ai.dev/api/markdown?path=docs/performance/monitoring
$cat docs/performance-monitoring.md
updated Recently·38 min read·published

Performance Monitoring

PerformanceLighthouseRUMProfilingBudgetsIntermediate to Advanced🎯Free Tools
Introduction

Performance monitoring is the practice of continuously measuring, tracking, and alerting on web performance metrics. Without monitoring, performance degrades silently as new features are added — a phenomenon known as performance entropy.

A complete monitoring strategy combines three pillars: lab testing (Lighthouse, WebPageTest) for controlled benchmarks, real-user monitoring (RUM) for production measurements, and profiling (DevTools, flame charts) for debugging specific issues.

Lighthouse Auditing

Lighthouse is Google's automated auditing tool for web performance, accessibility, SEO, and best practices. It provides a performance score (0-100) and actionable recommendations. It can be run from Chrome DevTools, CLI, or CI/CD pipelines.

lighthouse-setup.js
Bash
1# Lighthouse CLI — comprehensive performance audit
2npx lighthouse https://example.com \
3 --output=html \
4 --output-path=./reports/lighthouse.html \
5 --only-categories=performance \
6 --chrome-flags="--headless --no-sandbox"
7
8# Run with specific metrics
9npx lighthouse https://example.com \
10 --only-categories=performance \
11 --output=json \
12 --preset=desktop
13
14# CI/CD integration with Lighthouse CI
15npm install -g @lhci/cli
16
17# lhci autorun — runs collect, assert, and upload
18lhci autorun
19
20# lighthouserc.js configuration
21module.exports = {
22 ci: {
23 collect: {
24 url: ["http://localhost:3000"],
25 numberOfRuns: 3, // Average 3 runs for consistency
26 startServerCommand: "npm run start",
27 startServerReadyPattern: "ready on",
28 },
29 assert: {
30 assertions: {
31 // Core Web Vitals thresholds
32 "largest-contentful-paint": ["error", { maxNumericValue: 2500 }],
33 "cumulative-layout-shift": ["error", { maxNumericValue: 0.1 }],
34 "total-blocking-time": ["error", { maxNumericValue: 300 }],
35
36 // Overall score
37 "categories:performance": ["error", { minScore: 0.9 }],
38
39 // Resource budgets
40 "resource-summary:script:size": ["error", { maxNumericValue: 300000 }],
41 "resource-summary:stylesheet:size": ["error", { maxNumericValue: 50000 }],
42 "resource-summary:image:size": ["error", { maxNumericValue: 500000 }],
43 "resource-summary:total:size": ["error", { maxNumericValue: 1500000 }],
44 "resource-summary:third-party:transferSize": ["error", { maxNumericValue: 200000 }],
45 },
46 },
47 upload: {
48 target: "lhci",
49 serverBaseUrl: "https://lhci.example.com",
50 },
51 },
52};
lighthouse-analysis.ts
TypeScript
1// Custom Lighthouse scoring — understand what affects the score
2// Lighthouse 10+ scoring weights (approximate):
3
4// Largest Contentful Paint (LCP) — 25% of score
5// Interaction to Next Paint (INP) — 25% of score
6// Cumulative Layout Shift (CLS) — 25% of score
7// First Contentful Paint (FCP) — 10% of score
8// Total Blocking Time (TBT) — 10% of score
9// Speed Index — 5% of score
10
11// Scoring thresholds (Lighthouse 10):
12// Good: 90-100 (green)
13// Average: 50-89 (orange)
14// Poor: 0-49 (red)
15
16// Parse Lighthouse JSON report
17function parseLighthouseReport(reportPath: string) {
18 const report = JSON.parse(require("fs").readFileSync(reportPath, "utf-8"));
19
20 const metrics = {
21 score: report.categories.performance.score * 100,
22 LCP: report.audits["largest-contentful-paint"].numericValue,
23 CLS: report.audits["cumulative-layout-shift"].numericValue,
24 FCP: report.audits["first-contentful-paint"].numericValue,
25 TBT: report.audits["total-blocking-time"].numericValue,
26 SI: report.audits["speed-index"].numericValue,
27 };
28
29 // Get all opportunities (actionable recommendations)
30 const opportunities = Object.values(report.audits)
31 .filter((a: any) => a.details?.type === "opportunity" && a.score !== null)
32 .map((a: any) => ({
33 id: a.id,
34 title: a.title,
35 savings: a.details.overallSavingsMs,
36 savingsBytes: a.details.overallSavingsBytes,
37 }))
38 .sort((a: any, b: any) => b.savings - a.savings);
39
40 // Get all diagnostics
41 const diagnostics = Object.values(report.audits)
42 .filter((a: any) => a.details?.type === "table" && a.score !== null && a.score < 1)
43 .map((a: any) => ({
44 id: a.id,
45 title: a.title,
46 displayValue: a.displayValue,
47 }));
48
49 return { metrics, opportunities, diagnostics };
50}

info

Run Lighthouse in CI/CD with numberOfRuns: 3 to get consistent results. Single runs can vary by 10-15 points. Set performance budgets in the assert step to block deployments that regress performance.
Real User Monitoring (RUM)

Real User Monitoring measures performance from actual users in production. Unlike lab tools, RUM captures the full spectrum of devices, networks, and usage patterns. It is the ground truth for user experience.

rum-implementation.ts
TypeScript
1// RUM implementation with Performance Observer API
2// rum.js — Real User Monitoring library
3
4interface MetricData {
5 name: string;
6 value: number;
7 rating: "good" | "needs-improvement" | "poor";
8 delta: number;
9 id: string;
10 navigationType: string;
11 attribution?: any;
12}
13
14class RealUserMonitoring {
15 private endpoint: string;
16 private userId?: string;
17 private sessionId: string;
18 private metrics: Map<string, MetricData> = new Map();
19
20 constructor(endpoint: string, userId?: string) {
21 this.endpoint = endpoint;
22 this.userId = userId;
23 this.sessionId = this.generateId();
24 this.init();
25 }
26
27 private init() {
28 this.observeLCP();
29 this.observeFID();
30 this.observeCLS();
31 this.observeINP();
32 this.observeFCP();
33 this.observeTTFB();
34 this.observeNavigation();
35 this.observeResources();
36
37 // Send metrics when page is hidden (best time to send)
38 document.addEventListener("visibilitychange", () => {
39 if (document.visibilityState === "hidden") {
40 this.sendAllMetrics();
41 }
42 });
43
44 // Also send on page leave
45 window.addEventListener("pagehide", () => this.sendAllMetrics());
46 }
47
48 private observeLCP() {
49 const observer = new PerformanceObserver((entryList) => {
50 const entries = entryList.getEntries();
51 const lastEntry = entries[entries.length - 1] as any;
52
53 const value = lastEntry.startTime;
54 const rating = value <= 2500 ? "good" : value <= 4000 ? "needs-improvement" : "poor";
55
56 this.metrics.set("LCP", {
57 name: "LCP",
58 value,
59 rating,
60 delta: value,
61 id: this.generateId(),
62 navigationType: "navigate",
63 attribution: {
64 element: lastEntry.element?.tagName,
65 url: lastEntry.url,
66 size: lastEntry.size,
67 },
68 });
69 });
70
71 observer.observe({ type: "largest-contentful-paint", buffered: true });
72 }
73
74 private observeCLS() {
75 let clsValue = 0;
76 let sessionValue = 0;
77 let sessionEntries: any[] = [];
78
79 const observer = new PerformanceObserver((entryList) => {
80 for (const entry of entryList.getEntries()) {
81 if ((entry as any).hadRecentInput) continue;
82
83 if (entry.startTime - (sessionEntries[0]?.startTime || 0) < 1000 &&
84 entry.startTime - (sessionEntries[0]?.startTime || 0) < 5000) {
85 sessionValue += (entry as any).value;
86 sessionEntries.push(entry);
87 } else {
88 sessionValue = (entry as any).value;
89 sessionEntries = [entry];
90 }
91
92 if (sessionValue > clsValue) {
93 clsValue = sessionValue;
94 }
95 }
96
97 const rating = clsValue <= 0.1 ? "good" : clsValue <= 0.25 ? "needs-improvement" : "poor";
98
99 this.metrics.set("CLS", {
100 name: "CLS",
101 value: clsValue,
102 rating,
103 delta: clsValue,
104 id: this.generateId(),
105 navigationType: "navigate",
106 });
107 });
108
109 observer.observe({ type: "layout-shift", buffered: true });
110 }
111
112 private observeINP() {
113 let maxINP = 0;
114
115 const observer = new PerformanceObserver((entryList) => {
116 for (const entry of entryList.getEntries()) {
117 const inpEntry = entry as any;
118 const duration = inpEntry.processingEnd - inpEntry.startTime;
119 if (duration > maxINP) {
120 maxINP = duration;
121 }
122
123 const rating = maxINP <= 200 ? "good" : maxINP <= 500 ? "needs-improvement" : "poor";
124
125 this.metrics.set("INP", {
126 name: "INP",
127 value: maxINP,
128 rating,
129 delta: maxINP,
130 id: this.generateId(),
131 navigationType: "navigate",
132 });
133 }
134 });
135
136 observer.observe({ type: "event", buffered: true, durationThreshold: 16 });
137 }
138
139 private observeFID() {
140 const observer = new PerformanceObserver((entryList) => {
141 const firstInput = entryList.getEntries()[0] as any;
142 if (!firstInput) return;
143
144 const value = firstInput.processingStart - firstInput.startTime;
145 const rating = value <= 100 ? "good" : value <= 300 ? "needs-improvement" : "poor";
146
147 this.metrics.set("FID", {
148 name: "FID",
149 value,
150 rating,
151 delta: value,
152 id: this.generateId(),
153 navigationType: "navigate",
154 });
155 });
156
157 observer.observe({ type: "first-input", buffered: true });
158 }
159
160 private observeFCP() {
161 const observer = new PerformanceObserver((entryList) => {
162 for (const entry of entryList.getEntries()) {
163 if (entry.name === "first-contentful-paint") {
164 const value = entry.startTime;
165 const rating = value <= 1800 ? "good" : value <= 3000 ? "needs-improvement" : "poor";
166
167 this.metrics.set("FCP", {
168 name: "FCP",
169 value,
170 rating,
171 delta: value,
172 id: this.generateId(),
173 navigationType: "navigate",
174 });
175 }
176 }
177 });
178
179 observer.observe({ type: "paint", buffered: true });
180 }
181
182 private observeTTFB() {
183 const observer = new PerformanceObserver((entryList) => {
184 const nav = entryList.getEntries()[0] as PerformanceNavigationTiming;
185 if (!nav) return;
186
187 const value = nav.responseStart - nav.requestStart;
188 const rating = value <= 800 ? "good" : value <= 1800 ? "needs-improvement" : "poor";
189
190 this.metrics.set("TTFB", {
191 name: "TTFB",
192 value,
193 rating,
194 delta: value,
195 id: this.generateId(),
196 navigationType: nav.type,
197 });
198 });
199
200 observer.observe({ type: "navigation", buffered: true });
201 }
202
203 private observeNavigation() {
204 const observer = new PerformanceObserver((entryList) => {
205 const nav = entryList.getEntries()[0] as PerformanceNavigationTiming;
206 if (!nav) return;
207
208 this.metrics.set("Navigation", {
209 name: "Navigation",
210 value: nav.loadEventEnd - nav.startTime,
211 rating: "good",
212 delta: 0,
213 id: this.generateId(),
214 navigationType: nav.type,
215 attribution: {
216 redirect: nav.redirectEnd - nav.redirectStart,
217 dns: nav.domainLookupEnd - nav.domainLookupStart,
218 tcp: nav.connectEnd - nav.connectStart,
219 ttfb: nav.responseStart - nav.requestStart,
220 download: nav.responseEnd - nav.responseStart,
221 domInteractive: nav.domInteractive - nav.startTime,
222 domComplete: nav.domComplete - nav.startTime,
223 loadEvent: nav.loadEventEnd - nav.startTime,
224 },
225 });
226 });
227
228 observer.observe({ type: "navigation", buffered: true });
229 }
230
231 private observeResources() {
232 const observer = new PerformanceObserver((entryList) => {
233 const slowResources = entryList.getEntries()
234 .filter((e) => e.duration > 500)
235 .map((e) => ({
236 name: e.name,
237 duration: e.duration,
238 size: (e as any).transferSize,
239 }));
240
241 if (slowResources.length > 0) {
242 this.metrics.set("SlowResources", {
243 name: "SlowResources",
244 value: slowResources.length,
245 rating: "needs-improvement",
246 delta: 0,
247 id: this.generateId(),
248 navigationType: "navigate",
249 attribution: slowResources,
250 });
251 }
252 });
253
254 observer.observe({ type: "resource", buffered: true });
255 }
256
257 private async sendAllMetrics() {
258 const data = {
259 userId: this.userId,
260 sessionId: this.sessionId,
261 url: window.location.href,
262 userAgent: navigator.userAgent,
263 connection: (navigator as any).connection?.effectiveType,
264 deviceMemory: (navigator as any).deviceMemory,
265 hardwareConcurrency: navigator.hardwareConcurrency,
266 timestamp: Date.now(),
267 metrics: Object.fromEntries(this.metrics),
268 };
269
270 // Use sendBeacon for reliable delivery during page unload
271 if (navigator.sendBeacon) {
272 navigator.sendBeacon(this.endpoint, JSON.stringify(data));
273 } else {
274 fetch(this.endpoint, {
275 method: "POST",
276 body: JSON.stringify(data),
277 keepalive: true,
278 });
279 }
280 }
281
282 private generateId(): string {
283 return Math.random().toString(36).substring(2, 15);
284 }
285}
286
287// Initialize RUM
288const rum = new RealUserMonitoring(
289 "https://rum.example.com/collect",
290 undefined // userId (optional)
291);
RUM ProviderFree TierFeaturesBest For
web-vitals (npm)Free (self-hosted)CWV only, lightweightSimple integration, custom backends
Vercel AnalyticsIncluded in plansCWV, Speed Insights, dashboardNext.js / Vercel projects
Google Analytics 4FreeCustom events, segmentsGeneral analytics + perf
SpeedCurveTrial availableBudgets, alerts, competitor trackingEnterprise performance monitoring
Datadog RUM14-day trialFull observability, errors, tracesFull-stack observability

best practice

Use the web-vitals library for a lightweight, standardized way to measure Core Web Vitals. It handles all edge cases (buffered entries, INP interaction tracking, CLS session windows) and works with any analytics backend.
Performance Budgets

Performance budgets set hard limits on metrics that affect performance. They are enforced in CI/CD to prevent regressions from reaching production. Without budgets, performance degrades by 1-2% per sprint as features are added.

perf-budgets.js
TypeScript
1// size-limit — JavaScript bundle size budgets
2// .size-limit.json
3[
4 {
5 "name": "Initial bundle",
6 "path": "dist/index.js",
7 "import": "{ App }",
8 "limit": "120 KB",
9 "gzip": true
10 },
11 {
12 "name": "Vendor chunk",
13 "path": "dist/vendor.js",
14 "limit": "200 KB",
15 "gzip": true,
16 "ignore": ["react", "react-dom"]
17 },
18 {
19 "name": "CSS bundle",
20 "path": "dist/styles.css",
21 "limit": "30 KB",
22 "gzip": true
23 }
24]
25
26// package.json scripts
27// {
28// "scripts": {
29// "size": "size-limit",
30// "size:why": "size-limit --why"
31// }
32// }
33
34// bundlesize — CI bundle size checks
35// package.json
36// {
37// "bundlesize": [
38// { "path": "./dist/**/*.js", "maxSize": "50 kB", "compression": "gzip" },
39// { "path": "./dist/**/*.css", "maxSize": "20 kB", "compression": "gzip" }
40// ]
41// }
42
43// GitHub Actions — performance budget enforcement
44// .github/workflows/perf-budget.yml
45// name: Performance Budget
46// on: [pull_request]
47// jobs:
48// budget:
49// runs-on: ubuntu-latest
50// steps:
51// - uses: actions/checkout@v4
52// - uses: actions/setup-node@v4
53// - run: npm ci
54// - run: npm run build
55// - run: npx size-limit
56
57// Lighthouse CI budgets (in lighthouserc.js)
58// assert: {
59// assertions: {
60// "categories:performance": ["error", { minScore: 0.9 }],
61// "resource-summary:script:size": ["error", { maxNumericValue: 300000 }],
62// }
63// }
MetricBudgetRationale
Total JS (gzip)≤ 170KBSub-second parse on mid-range phones
Total CSS (gzip)≤ 30KBFast style calculation, minimal render-blocking
Total page weight≤ 1.5MB3G load in under 5 seconds
Third-party JS≤ 100KBThird-party scripts are uncontrolled
Image weight≤ 500KBAbove-the-fold images with lazy loading
Request count≤ 50Minimize HTTP overhead
LCP≤ 2.5sGoogle Core Web Vitals threshold
INP≤ 200msResponsive interactivity
CLS≤ 0.1Visual stability
🔥

pro tip

Start with generous budgets and tighten over time. If you set budgets too aggressively from day one, teams will ignore them. A good approach: set budgets at 120% of current values, then reduce by 5% each quarter.
Chrome DevTools Profiling

Chrome DevTools provides the most detailed performance profiling capabilities. The Performance panel records everything happening in the browser — JavaScript execution, layout, paint, and compositing — and visualizes it as a flame chart.

devtools-guide.txt
TEXT
1Chrome DevTools Performance Panel — Key Features:
2
3 1. Recording
4 - Click record, interact with page, stop recording
5 - Enable "Screenshots" for visual timeline
6 - Enable "Memory" for heap snapshot comparison
7 - Throttle CPU (4x or 6x) to simulate mid-range devices
8 - Throttle network (Slow 3G) to simulate slow connections
9
10 2. Flame Chart (Main Thread)
11 - X-axis: Time (left to right)
12 - Y-axis: Call stack depth
13 - Color coding:
14 - Yellow: JavaScript execution
15 - Purple: Layout (recalculation)
16 - Green: Paint
17 - Blue: Composite (GPU)
18 - Wide blocks = long tasks = jank sources
19
20 3. Summary Tab
21 - Breakdown of time spent in each category
22 - Shows total: Scripting, Rendering, Painting, System
23
24 4. Bottom-Up Tab
25 - Shows which functions took the most total time
26 - Group by "Total Time" to find bottlenecks
27
28 5. Call Tree Tab
29 - Hierarchical view of function calls
30 - Self-time vs total time columns
31
32 6. Long Tasks
33 - Red triangles in the timeline indicate long tasks (>50ms)
34 - Click on a long task to see what code was executing
35 - Long tasks block the main thread and cause jank
36
37 7. Memory Panel
38 - Heap size over time (should return to baseline after GC)
39 - Take heap snapshots to find memory leaks
40 - Compare snapshots to see what objects are growing
devtools-profiling.ts
TypeScript
1// Programmatic long task detection
2function detectLongTasks() {
3 const longTasks: { duration: number; startTime: number; scripts: string[] }[] = [];
4
5 const observer = new PerformanceObserver((entryList) => {
6 for (const entry of entryList.getEntries()) {
7 if (entry.duration > 50) {
8 const longTask = {
9 duration: entry.duration,
10 startTime: entry.startTime,
11 scripts: [], // Attribution data when available
12 };
13
14 // Get attribution (which scripts caused the long task)
15 if ((entry as any).attribution) {
16 longTask.scripts = (entry as any).attribution.map(
17 (a: any) => a.scriptURL || "unknown"
18 );
19 }
20
21 longTasks.push(longTask);
22
23 console.warn(
24 "Long task detected:",
25 entry.duration.toFixed(1) + "ms",
26 "at", entry.startTime.toFixed(0) + "ms",
27 longTask.scripts.length > 0 ? "caused by: " + longTask.scripts.join(", ") : ""
28 );
29 }
30 }
31 });
32
33 observer.observe({ type: "longtask", buffered: true });
34
35 return {
36 getTasks: () => longTasks,
37 getSummary: () => ({
38 count: longTasks.length,
39 totalBlockingTime: longTasks.reduce(
40 (sum, t) => sum + Math.max(0, t.duration - 50), 0
41 ),
42 worstTask: Math.max(0, ...longTasks.map((t) => t.duration)),
43 }),
44 };
45}
46
47// Memory leak detection
48function detectMemoryLeaks() {
49 const snapshots: { timestamp: number; heapSize: number }[] = [];
50
51 if ("memory" in performance) {
52 const mem = (performance as any).memory;
53 snapshots.push({
54 timestamp: Date.now(),
55 heapSize: mem.usedJSHeapSize,
56 });
57
58 setInterval(() => {
59 const current = {
60 timestamp: Date.now(),
61 heapSize: mem.usedJSHeapSize,
62 };
63 snapshots.push(current);
64
65 // Check for steady growth (potential leak)
66 if (snapshots.length > 10) {
67 const recent = snapshots.slice(-10);
68 const growth = recent[recent.length - 1].heapSize - recent[0].heapSize;
69 const avgGrowth = growth / recent.length;
70
71 if (avgGrowth > 100000) { // >100KB per sample
72 console.warn("Potential memory leak: avg growth",
73 (avgGrowth / 1024).toFixed(1) + "KB/sample");
74 }
75 }
76 }, 5000);
77 }
78}

info

Use Chrome DevTools with CPU throttling (4x slowdown) to simulate mid-range mobile devices. Most performance issues are invisible on fast development machines. Always profile under realistic constraints.
Reading Flame Charts

Flame charts are the primary visualization for performance profiling. Understanding how to read them is essential for identifying bottlenecks. Each horizontal block represents a function call — wider blocks mean more time spent.

flame-chart-guide.txt
TEXT
1Flame Chart Reading Guide:
2
3 Top-level layout:
4 ┌──────────────────────────────────────────────────┐
5 │ Navigation / Raster │ ← Browser threads
6 ├──────────────────────────────────────────────────┤
7 │ Main Thread │ ← Your JS + layout + paint
8 │ ┌─────────────────────────────────────────────┐ │
9 │ │ Scripting (yellow) │ │ ← JS execution
10 │ │ ┌──────────────────────────────────────────┐│ │
11 │ │ │ Function A (50ms) ││ │
12 │ │ │ ┌──────────────────────────────────────┐ ││ │
13 │ │ │ │ Function B (30ms) │ ││ │
14 │ │ │ │ ┌────────────────────────────────┐ │ ││ │
15 │ │ │ │ │ Function C (15ms) │ │ ││ │
16 │ │ │ │ │ ┌──────────────────────────┐ │ │ ││ │
17 │ │ │ │ │ │ Function D (5ms) │ │ │ ││ │
18 │ │ │ │ │ └──────────────────────────┘ │ │ ││ │
19 │ │ │ │ └────────────────────────────────┘ │ ││ │
20 │ │ │ └──────────────────────────────────────┘ ││ │
21 │ │ └──────────────────────────────────────────┘│ │
22 │ ├─────────────────────────────────────────────┤ │
23 │ │ Layout (purple) │ │ ← Style + layout
24 │ ├─────────────────────────────────────────────┤ │
25 │ │ Paint (green) │ │ ← Rasterization
26 │ ├─────────────────────────────────────────────┤ │
27 │ │ Composite (blue) │ │ ← GPU layer composition
28 │ └─────────────────────────────────────────────┘ │
29 └──────────────────────────────────────────────────┘
30
31 Key patterns to look for:
32
33 1. Long yellow blocks (Scripting)
34 → JavaScript is blocking the main thread
35 → Look for: heavy computations, DOM manipulation, event handlers
36 → Fix: break up long tasks, use web workers, debouncing
37
38 2. Tall purple blocks (Layout)
39 → Expensive layout recalculation
40 → Look for: many DOM elements, forced reflows, complex CSS
41 → Fix: reduce DOM size, use CSS containment, batch DOM writes
42
43 3. Wide green blocks (Paint)
44 → Expensive rendering
45 → Look for: large areas, blur filters, box shadows
46 → Fix: reduce paint area, use transform/opacity, GPU layers
47
48 4. Red triangles on top (Long Tasks)
49 → Tasks exceeding 50ms that block the main thread
50 → These directly impact INP and user responsiveness
51 → Fix: break up with setTimeout, scheduler.yield(), web workers
52
53 5. Gaps between blocks (Idle time)
54 → Good! Main thread is not overloaded
55 → If you see this during interaction, the issue is elsewhere
56
57 6. Repeated identical patterns (Loops)
58 → Same code running repeatedly
59 → Check if it is intentional or a bug
60 → Consider caching or memoization
programmatic-profiling.ts
TypeScript
1// Programmatic profiling with Performance API
2class PerformanceProfiler {
3 private marks: Map<string, number> = new Map();
4 private measures: { name: string; duration: number; start: number }[] = [];
5
6 mark(name: string) {
7 performance.mark(name);
8 this.marks.set(name, performance.now());
9 }
10
11 measure(name: string, startMark: string, endMark?: string) {
12 const end = endMark || name + "-end";
13 if (!this.marks.has(end)) {
14 performance.mark(end);
15 }
16
17 performance.measure(name, startMark, end);
18 const measure = performance.getEntriesByName(name).pop();
19
20 if (measure) {
21 this.measures.push({
22 name,
23 duration: measure.duration,
24 start: measure.startTime,
25 });
26
27 if (measure.duration > 16) { // Longer than one frame
28 console.warn("Slow operation:", name, measure.duration.toFixed(1) + "ms");
29 }
30 }
31
32 return measure?.duration || 0;
33 }
34
35 // Profile a function
36 async profile<T>(name: string, fn: () => T | Promise<T>): Promise<T> {
37 this.mark(name + "-start");
38 const result = await fn();
39 this.measure(name, name + "-start", name + "-end");
40 return result;
41 }
42
43 // Get summary of all measurements
44 getSummary() {
45 const sorted = [...this.measures].sort((a, b) => b.duration - a.duration);
46 const total = sorted.reduce((sum, m) => sum + m.duration, 0);
47
48 console.table(sorted.map((m) => ({
49 name: m.name,
50 duration: m.duration.toFixed(1) + "ms",
51 percentage: ((m.duration / total) * 100).toFixed(1) + "%",
52 })));
53
54 return { measures: sorted, total };
55 }
56
57 // Clear all marks and measures
58 clear() {
59 this.marks.clear();
60 this.measures = [];
61 performance.clearMarks();
62 performance.clearMeasures();
63 }
64}
65
66// Usage
67const profiler = new PerformanceProfiler();
68
69// Profile an async operation
70const data = await profiler.profile("fetch-products", async () => {
71 const res = await fetch("/api/products");
72 return res.json();
73});
74
75// Profile a synchronous operation
76profiler.profile("render-list", () => {
77 renderProductList(data);
78});
79
80// Get summary after all operations
81profiler.getSummary();
Chrome User Experience Report (CrUX)

CrUX is Google's dataset of real-user Core Web Vitals data for millions of websites. It is the source of data for PageSpeed Insights and the basis for Google's search ranking signals. CrUX data is collected from Chrome users who opt in to usage statistics.

crux-data.ts
TypeScript
1// CrUX API — fetch real-user metrics for your origin
2async function fetchCrUXData(origin: string) {
3 const API_KEY = "YOUR_CRUX_API_KEY";
4
5 const response = await fetch(
6 "https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=" + API_KEY,
7 {
8 method: "POST",
9 headers: { "Content-Type": "application/json" },
10 body: JSON.stringify({ url: origin }),
11 }
12 );
13
14 const data = await response.json();
15
16 // Parse Core Web Vitals from CrUX response
17 const metrics = {
18 LCP: {
19 p75: data.record.metrics.largestContentfulPaint?.percentiles?.p75,
20 distributions: data.record.metrics.largestContentfulPaint?.distributions,
21 // distributions: [{min, max, proportion}, ...]
22 // proportion is % of page loads in this range
23 },
24 INP: {
25 p75: data.record.metrics.interactionToNextPaint?.percentiles?.p75,
26 distributions: data.record.metrics.interactionToNextPaint?.distributions,
27 },
28 CLS: {
29 p75: data.record.metrics.cumulativeLayoutShift?.percentiles?.p75,
30 distributions: data.record.metrics.cumulativeLayoutShift?.distributions,
31 },
32 FCP: {
33 p75: data.record.metrics.firstContentfulPaint?.percentiles?.p75,
34 },
35 TTFB: {
36 p75: data.record.metrics.experimentalTimeToFirstByte?.percentiles?.p75,
37 },
38 };
39
40 // Calculate "good" percentage (percentage of page loads with good CWV)
41 const lcpGood = metrics.LCP.distributions?.[0]?.proportion || 0;
42 const inpGood = metrics.INP.distributions?.[0]?.proportion || 0;
43 const clsGood = metrics.CLS.distributions?.[0]?.proportion || 0;
44
45 const goodCWV = Math.min(lcpGood, inpGood, clsGood);
46
47 console.log("CrUX Data for", origin);
48 console.log(" LCP p75:", (metrics.LCP.p75 / 1000).toFixed(1) + "s");
49 console.log(" INP p75:", metrics.INP.p75 + "ms");
50 console.log(" CLS p75:", metrics.CLS.p75.toFixed(3));
51 console.log(" Good CWV:", (goodCWV * 100).toFixed(1) + "%");
52
53 return { metrics, goodCWV };
54}
55
56// BigQuery — historical CrUX data
57// Query: SELECT * FROM `chrome-ux-report.all.202401`
58// WHERE origin = 'https://example.com'
59
60// CrUX Dashboard — visual trends over time
61// https://developer.chrome.com/docs/crux/

warning

CrUX data is aggregated over 28 days and only includes Chrome users who opt in to usage statistics. It may not represent your full user base. Use it for trend tracking, not for debugging individual sessions.
Performance Alerting

Monitoring without alerting is just dashboards that nobody looks at. Set up automated alerts for performance regressions so the team is notified before users complain. Alert on both synthetic (Lighthouse CI) and real-user (RUM) metrics.

perf-alerting.ts
TypeScript
1// Performance alert configuration
2// Alert when Core Web Vitals degrade in production
3
4interface AlertRule {
5 metric: string;
6 threshold: number;
7 window: string; // e.g., "1h", "24h", "7d"
8 severity: "warning" | "critical";
9 notify: string[];
10}
11
12const alertRules: AlertRule[] = [
13 {
14 metric: "LCP_p75",
15 threshold: 2500, // Alert if p75 exceeds 2.5s
16 window: "1h",
17 severity: "warning",
18 notify: ["slack:#performance", "email:team@example.com"],
19 },
20 {
21 metric: "LCP_p75",
22 threshold: 4000, // Critical if p75 exceeds 4s
23 window: "1h",
24 severity: "critical",
25 notify: ["slack:#performance", "pagerduty:oncall"],
26 },
27 {
28 metric: "INP_p75",
29 threshold: 200,
30 window: "1h",
31 severity: "warning",
32 notify: ["slack:#performance"],
33 },
34 {
35 metric: "CLS_p75",
36 threshold: 0.1,
37 window: "24h",
38 severity: "warning",
39 notify: ["slack:#performance"],
40 },
41 {
42 metric: "bundle_size_bytes",
43 threshold: 300000, // Alert if JS bundle exceeds 300KB gzipped
44 window: "deploy",
45 severity: "critical",
46 notify: ["slack:#engineering", "github:pr-comment"],
47 },
48];
49
50// Simple alerting function
51function checkAlerts(currentMetrics: Record<string, number>) {
52 const alerts: { rule: AlertRule; currentValue: number }[] = [];
53
54 for (const rule of alertRules) {
55 const currentValue = currentMetrics[rule.metric];
56 if (currentValue !== undefined && currentValue > rule.threshold) {
57 alerts.push({ rule, currentValue });
58 }
59 }
60
61 for (const alert of alerts) {
62 const message =
63 "[" + alert.rule.severity.toUpperCase() + "] " +
64 alert.rule.metric + " = " + alert.currentValue +
65 " (threshold: " + alert.rule.threshold + ")";
66
67 console.warn(message);
68
69 // Send to notification channels
70 for (const channel of alert.rule.notify) {
71 sendNotification(channel, message);
72 }
73 }
74
75 return alerts;
76}
77
78function sendNotification(channel: string, message: string) {
79 // Implementation depends on channel type
80 // Slack webhook, email, PagerDuty API, GitHub API, etc.
81 console.log("Alert sent to", channel, ":", message);
82}

best practice

Alert on user-impacting thresholds, not arbitrary numbers. LCP > 2.5s means 25% of your users are having a poor experience. INP > 200ms means interactions feel sluggish. Tie alerts to business impact.
Best Practices
Use Lighthouse in CI/CD with performance budgets to catch regressions before deployment
Implement RUM in production to measure real user experience across all devices
Set up automated alerts for Core Web Vitals degradations — don't rely on manual checks
Profile with Chrome DevTools under CPU throttling (4x) to simulate real devices
Use flame charts to identify long tasks — they are the #1 cause of interaction jank
Track CrUX data monthly to monitor your site's Core Web Vitals trend
Run Lighthouse audits on every PR and block merges if the score drops below 90
Use the web-vitals library for standardized, cross-browser metric collection
Measure performance on real devices (not just development machines) — use DevTools throttling
Share performance dashboards with the whole team — make performance visible and accountable
$Blueprint — Engineering Documentation·Section ID: PERF-MON-01·Revision: 1.0