前端性能监控看板的搭建Web Vitals Grafana 自定义指标的完整方案性能监控不应只是 Lighthouse 的一次性报告。本文提供一套基于 Web Vitals API 和 Grafana 的实时性能监控方案帮助团队建立从数据采集到可视化告警的完整链路。flowchart LR subgraph 数据采集层 A[Web Vitals API] -- D[性能 Beacon] B[自定义指标] -- D C[错误与异常] -- D end subgraph 数据处理层 D -- E[上报网关br/Nginx / Node] E -- F[时序数据库br/InfluxDB / Prometheus] end subgraph 可视化与告警层 F -- G[Grafana 看板] F -- H[告警规则] H -- I[企业微信 / 钉钉] end style D fill:#4A90D9,color:#fff style F fill:#F5A623,color:#fff style G fill:#50C878,color:#fff一、Web Vitals 核心指标采集Google 的 Web Vitals 定义了三个核心指标LCP最大内容绘制、FID/INP交互延迟和 CLS累积布局偏移。使用web-vitals库可以便捷地采集这些指标。// performance-collector.ts - 性能数据采集模块 import { onLCP, onFID, onINP, onCLS, onTTFB } from web-vitals; interface VitalMetric { name: string; value: number; rating: good | needs-improvement | poor; delta: number; id: string; navigationType: string; timestamp: number; } interface PerformancePayload { vitals: VitalMetric[]; customMetrics: Recordstring, number; pageInfo: { url: string; referrer: string; screenWidth: number; screenHeight: number; effectiveType: string; // 网络类型 deviceMemory: number; // 设备内存 GB }; timestamp: number; } class PerformanceCollector { private vitals: VitalMetric[] []; private customMetrics: Recordstring, number {}; // 采集 Core Web Vitals startVitalsCollection(): void { const ratingThresholds { LCP: [2500, 4000], FID: [100, 300], INP: [200, 500], CLS: [0.1, 0.25], TTFB: [800, 1800], }; const getRating ( name: string, value: number ): VitalMetric[rating] { const thresholds ratingThresholds[name as keyof typeof ratingThresholds]; if (!thresholds) return good; if (value thresholds[0]) return good; if (value thresholds[1]) return needs-improvement; return poor; }; // 使用标准 Web Vitals API 收集指标 onCLS((metric) { this.vitals.push({ name: CLS, value: metric.value, rating: getRating(CLS, metric.value), delta: metric.delta, id: metric.id, navigationType: metric.navigationType, timestamp: Date.now(), }); }); onLCP((metric) { this.vitals.push({ name: LCP, value: metric.value, rating: getRating(LCP, metric.value), delta: metric.delta, id: metric.id, navigationType: metric.navigationType, timestamp: Date.now(), }); }); onINP((metric) { this.vitals.push({ name: INP, value: metric.value, rating: getRating(INP, metric.value), delta: metric.delta, id: metric.id, navigationType: metric.navigationType, timestamp: Date.now(), }); }); onTTFB((metric) { this.vitals.push({ name: TTFB, value: metric.value, rating: getRating(TTFB, metric.value), delta: metric.delta, id: metric.id, navigationType: metric.navigationType, timestamp: Date.now(), }); }); } // 注册自定义指标 registerCustomMetric(name: string, value: number): void { this.customMetrics[name] value; } // 构建上报数据 buildPayload(): PerformancePayload { return { vitals: this.vitals, customMetrics: this.customMetrics, pageInfo: { url: location.href, referrer: document.referrer, screenWidth: screen.width, screenHeight: screen.height, effectiveType: (navigator as Navigator { connection?: { effectiveType: string } }) .connection?.effectiveType ?? unknown, deviceMemory: (navigator as Navigator { deviceMemory?: number }).deviceMemory ?? 0, }, timestamp: Date.now(), }; } // 使用 sendBeacon 上报数据 sendBeacon(endpoint: string): boolean { const payload this.buildPayload(); const blob new Blob([JSON.stringify(payload)], { type: application/json, }); if (navigator.sendBeacon) { return navigator.sendBeacon(endpoint, blob); } // 降级方案使用 fetch keepalive fetch(endpoint, { method: POST, body: blob, keepalive: true, headers: { Content-Type: application/json }, }).catch((err) { console.error(性能数据上报失败:, err); }); return true; } }二、自定义业务指标除了 Web Vitals业务指标同样重要。以下是常见的前端自定义指标及其采集方式。// 自定义指标采集示例 class CustomMetricsCollector { private collector: PerformanceCollector; constructor(collector: PerformanceCollector) { this.collector collector; } // 首屏加载时间从导航开始到首屏关键内容渲染完成 measureFirstScreenTime(): void { if (document.readyState complete) { this.calculateFirstScreen(); } else { window.addEventListener(load, () { // 延迟一帧确保首屏渲染完成 requestAnimationFrame(() { setTimeout(() this.calculateFirstScreen(), 0); }); }); } } private calculateFirstScreen(): void { const navigationEntry performance.getEntriesByType( navigation )[0] as PerformanceNavigationTiming; if (navigationEntry) { this.collector.registerCustomMetric( first_screen_time, navigationEntry.domContentLoadedEventEnd ); } } // API 请求平均耗时使用 PerformanceObserver measureAPILatency(): void { try { const observer new PerformanceObserver((list) { const apiEntries list .getEntries() .filter( (entry) entry.name.includes(/api/) || entry.name.includes(/graphql) ); if (apiEntries.length 0) { const avgDuration apiEntries.reduce((s, e) s e.duration, 0) / apiEntries.length; this.collector.registerCustomMetric( avg_api_latency, Math.round(avgDuration) ); } }); observer.observe({ type: resource, buffered: true, }); } catch (err) { console.warn(API 延迟监控初始化失败:, err); } } // JavaScript 错误率 measureErrorRate(): void { let errorCount 0; // 使用 PerformanceObserver 不会停止页面卸载 window.addEventListener(error, () { errorCount; this.collector.registerCustomMetric( js_error_count, errorCount ); }); window.addEventListener(unhandledrejection, () { errorCount; this.collector.registerCustomMetric( unhandled_rejection_count, errorCount ); }); } }三、Node.js 上报网关前端采集的数据需要一个可靠的上报网关来接收、验证和转发。// report-server.ts - 上报网关Express 实现 import express from express; import { InfluxDB, Point } from influxdata/influxdb-client; const app express(); app.use(express.json({ limit: 50kb })); // InfluxDB 客户端配置 const influxDB new InfluxDB({ url: process.env.INFLUXDB_URL ?? http://localhost:8086, token: process.env.INFLUXDB_TOKEN ?? , }); const writeApi influxDB.getWriteApi( process.env.INFLUXDB_ORG ?? my-org, process.env.INFLUXDB_BUCKET ?? web-vitals, ms // 精度毫秒 ); // 性能数据上报接口 app.post(/api/report/performance, (req, res) { try { const payload req.body; const points: Point[] []; // 写入 Web Vitals 指标 for (const vital of payload.vitals ?? []) { const point new Point(web_vital) .tag(metric_name, vital.name) .tag(rating, vital.rating) .tag(url, payload.pageInfo?.url ?? unknown) .tag(network_type, payload.pageInfo?.effectiveType ?? unknown) .floatField(value, vital.value) .floatField(delta, vital.delta) .timestamp(new Date(vital.timestamp)); points.push(point); } // 写入自定义指标 for (const [key, value] of Object.entries( payload.customMetrics ?? {} )) { const point new Point(custom_metric) .tag(metric_name, key) .tag(url, payload.pageInfo?.url ?? unknown) .floatField(value, value as number) .timestamp(new Date(payload.timestamp)); points.push(point); } // 批量写入 writeApi.writePoints(points); // 异步 flush不阻塞响应 writeApi.flush().catch((err) { console.error(InfluxDB 写入失败:, err.message); }); res.status(204).end(); } catch (err) { console.error(上报数据处理异常:, err); res.status(400).json({ error: 数据格式错误 }); } }); // 优雅关闭 process.on(SIGTERM, async () { console.log(正在关闭上报服务…); try { await writeApi.close(); } catch (err) { console.error(InfluxDB 关闭异常:, err); } process.exit(0); }); const PORT process.env.PORT ?? 3001; app.listen(PORT, () { console.log(性能上报网关已启动: http://localhost:${PORT}); });四、Grafana 看板配置Grafana 连接到 InfluxDB 后通过 Flux 查询语言配置可视化面板。以下是对应的 Flux 查询示例用于计算 P75 的 LCP 时间// Grafana Panel Query - LCP P75 时间趋势 from(bucket: web-vitals) | range(start: v.timeRangeStart, stop: v.timeRangeStop) | filter(fn: (r) r._measurement web_vital) | filter(fn: (r) r.metric_name LCP) | filter(fn: (r) r._field value) | aggregateWindow(every: 5m, fn: (tables-, column) tables | quantile(q: 0.75)) | yield(name: LCP_P75)告警规则示例LCP 超过 2.5 秒时触发在 Grafana Alerting 中配置条件LCP_P75 2500评估间隔5 分钟通知渠道企业微信 / 钉钉 Webhook告警信息LCP 指标恶化当前 P75 值为 {{ $values.B.Value }}ms五、总结性能监控从单次的 Lighthouse 检查演进为持续的过程监控核心价值在于趋势分析和问题预警。搭建链路可以归纳为四个部分Web Vitals 自定义指标采集、sendBeacon 上报、InfluxDB 时序存储、Grafana 可视化与告警。关键的工程细节包括使用 sendBeacon 保证卸载时的数据不丢失、在上报网关中做数据验证和限流、在 Grafana 中为核心指标配置 P75/P95 而非平均值。通过这些措施可以建立起从感觉变慢了到数据显示 LCP 从 2.1s 恶化到 3.4s的精确度量闭环。
前端性能监控看板的搭建:Web Vitals + Grafana + 自定义指标的完整方案
前端性能监控看板的搭建Web Vitals Grafana 自定义指标的完整方案性能监控不应只是 Lighthouse 的一次性报告。本文提供一套基于 Web Vitals API 和 Grafana 的实时性能监控方案帮助团队建立从数据采集到可视化告警的完整链路。flowchart LR subgraph 数据采集层 A[Web Vitals API] -- D[性能 Beacon] B[自定义指标] -- D C[错误与异常] -- D end subgraph 数据处理层 D -- E[上报网关br/Nginx / Node] E -- F[时序数据库br/InfluxDB / Prometheus] end subgraph 可视化与告警层 F -- G[Grafana 看板] F -- H[告警规则] H -- I[企业微信 / 钉钉] end style D fill:#4A90D9,color:#fff style F fill:#F5A623,color:#fff style G fill:#50C878,color:#fff一、Web Vitals 核心指标采集Google 的 Web Vitals 定义了三个核心指标LCP最大内容绘制、FID/INP交互延迟和 CLS累积布局偏移。使用web-vitals库可以便捷地采集这些指标。// performance-collector.ts - 性能数据采集模块 import { onLCP, onFID, onINP, onCLS, onTTFB } from web-vitals; interface VitalMetric { name: string; value: number; rating: good | needs-improvement | poor; delta: number; id: string; navigationType: string; timestamp: number; } interface PerformancePayload { vitals: VitalMetric[]; customMetrics: Recordstring, number; pageInfo: { url: string; referrer: string; screenWidth: number; screenHeight: number; effectiveType: string; // 网络类型 deviceMemory: number; // 设备内存 GB }; timestamp: number; } class PerformanceCollector { private vitals: VitalMetric[] []; private customMetrics: Recordstring, number {}; // 采集 Core Web Vitals startVitalsCollection(): void { const ratingThresholds { LCP: [2500, 4000], FID: [100, 300], INP: [200, 500], CLS: [0.1, 0.25], TTFB: [800, 1800], }; const getRating ( name: string, value: number ): VitalMetric[rating] { const thresholds ratingThresholds[name as keyof typeof ratingThresholds]; if (!thresholds) return good; if (value thresholds[0]) return good; if (value thresholds[1]) return needs-improvement; return poor; }; // 使用标准 Web Vitals API 收集指标 onCLS((metric) { this.vitals.push({ name: CLS, value: metric.value, rating: getRating(CLS, metric.value), delta: metric.delta, id: metric.id, navigationType: metric.navigationType, timestamp: Date.now(), }); }); onLCP((metric) { this.vitals.push({ name: LCP, value: metric.value, rating: getRating(LCP, metric.value), delta: metric.delta, id: metric.id, navigationType: metric.navigationType, timestamp: Date.now(), }); }); onINP((metric) { this.vitals.push({ name: INP, value: metric.value, rating: getRating(INP, metric.value), delta: metric.delta, id: metric.id, navigationType: metric.navigationType, timestamp: Date.now(), }); }); onTTFB((metric) { this.vitals.push({ name: TTFB, value: metric.value, rating: getRating(TTFB, metric.value), delta: metric.delta, id: metric.id, navigationType: metric.navigationType, timestamp: Date.now(), }); }); } // 注册自定义指标 registerCustomMetric(name: string, value: number): void { this.customMetrics[name] value; } // 构建上报数据 buildPayload(): PerformancePayload { return { vitals: this.vitals, customMetrics: this.customMetrics, pageInfo: { url: location.href, referrer: document.referrer, screenWidth: screen.width, screenHeight: screen.height, effectiveType: (navigator as Navigator { connection?: { effectiveType: string } }) .connection?.effectiveType ?? unknown, deviceMemory: (navigator as Navigator { deviceMemory?: number }).deviceMemory ?? 0, }, timestamp: Date.now(), }; } // 使用 sendBeacon 上报数据 sendBeacon(endpoint: string): boolean { const payload this.buildPayload(); const blob new Blob([JSON.stringify(payload)], { type: application/json, }); if (navigator.sendBeacon) { return navigator.sendBeacon(endpoint, blob); } // 降级方案使用 fetch keepalive fetch(endpoint, { method: POST, body: blob, keepalive: true, headers: { Content-Type: application/json }, }).catch((err) { console.error(性能数据上报失败:, err); }); return true; } }二、自定义业务指标除了 Web Vitals业务指标同样重要。以下是常见的前端自定义指标及其采集方式。// 自定义指标采集示例 class CustomMetricsCollector { private collector: PerformanceCollector; constructor(collector: PerformanceCollector) { this.collector collector; } // 首屏加载时间从导航开始到首屏关键内容渲染完成 measureFirstScreenTime(): void { if (document.readyState complete) { this.calculateFirstScreen(); } else { window.addEventListener(load, () { // 延迟一帧确保首屏渲染完成 requestAnimationFrame(() { setTimeout(() this.calculateFirstScreen(), 0); }); }); } } private calculateFirstScreen(): void { const navigationEntry performance.getEntriesByType( navigation )[0] as PerformanceNavigationTiming; if (navigationEntry) { this.collector.registerCustomMetric( first_screen_time, navigationEntry.domContentLoadedEventEnd ); } } // API 请求平均耗时使用 PerformanceObserver measureAPILatency(): void { try { const observer new PerformanceObserver((list) { const apiEntries list .getEntries() .filter( (entry) entry.name.includes(/api/) || entry.name.includes(/graphql) ); if (apiEntries.length 0) { const avgDuration apiEntries.reduce((s, e) s e.duration, 0) / apiEntries.length; this.collector.registerCustomMetric( avg_api_latency, Math.round(avgDuration) ); } }); observer.observe({ type: resource, buffered: true, }); } catch (err) { console.warn(API 延迟监控初始化失败:, err); } } // JavaScript 错误率 measureErrorRate(): void { let errorCount 0; // 使用 PerformanceObserver 不会停止页面卸载 window.addEventListener(error, () { errorCount; this.collector.registerCustomMetric( js_error_count, errorCount ); }); window.addEventListener(unhandledrejection, () { errorCount; this.collector.registerCustomMetric( unhandled_rejection_count, errorCount ); }); } }三、Node.js 上报网关前端采集的数据需要一个可靠的上报网关来接收、验证和转发。// report-server.ts - 上报网关Express 实现 import express from express; import { InfluxDB, Point } from influxdata/influxdb-client; const app express(); app.use(express.json({ limit: 50kb })); // InfluxDB 客户端配置 const influxDB new InfluxDB({ url: process.env.INFLUXDB_URL ?? http://localhost:8086, token: process.env.INFLUXDB_TOKEN ?? , }); const writeApi influxDB.getWriteApi( process.env.INFLUXDB_ORG ?? my-org, process.env.INFLUXDB_BUCKET ?? web-vitals, ms // 精度毫秒 ); // 性能数据上报接口 app.post(/api/report/performance, (req, res) { try { const payload req.body; const points: Point[] []; // 写入 Web Vitals 指标 for (const vital of payload.vitals ?? []) { const point new Point(web_vital) .tag(metric_name, vital.name) .tag(rating, vital.rating) .tag(url, payload.pageInfo?.url ?? unknown) .tag(network_type, payload.pageInfo?.effectiveType ?? unknown) .floatField(value, vital.value) .floatField(delta, vital.delta) .timestamp(new Date(vital.timestamp)); points.push(point); } // 写入自定义指标 for (const [key, value] of Object.entries( payload.customMetrics ?? {} )) { const point new Point(custom_metric) .tag(metric_name, key) .tag(url, payload.pageInfo?.url ?? unknown) .floatField(value, value as number) .timestamp(new Date(payload.timestamp)); points.push(point); } // 批量写入 writeApi.writePoints(points); // 异步 flush不阻塞响应 writeApi.flush().catch((err) { console.error(InfluxDB 写入失败:, err.message); }); res.status(204).end(); } catch (err) { console.error(上报数据处理异常:, err); res.status(400).json({ error: 数据格式错误 }); } }); // 优雅关闭 process.on(SIGTERM, async () { console.log(正在关闭上报服务…); try { await writeApi.close(); } catch (err) { console.error(InfluxDB 关闭异常:, err); } process.exit(0); }); const PORT process.env.PORT ?? 3001; app.listen(PORT, () { console.log(性能上报网关已启动: http://localhost:${PORT}); });四、Grafana 看板配置Grafana 连接到 InfluxDB 后通过 Flux 查询语言配置可视化面板。以下是对应的 Flux 查询示例用于计算 P75 的 LCP 时间// Grafana Panel Query - LCP P75 时间趋势 from(bucket: web-vitals) | range(start: v.timeRangeStart, stop: v.timeRangeStop) | filter(fn: (r) r._measurement web_vital) | filter(fn: (r) r.metric_name LCP) | filter(fn: (r) r._field value) | aggregateWindow(every: 5m, fn: (tables-, column) tables | quantile(q: 0.75)) | yield(name: LCP_P75)告警规则示例LCP 超过 2.5 秒时触发在 Grafana Alerting 中配置条件LCP_P75 2500评估间隔5 分钟通知渠道企业微信 / 钉钉 Webhook告警信息LCP 指标恶化当前 P75 值为 {{ $values.B.Value }}ms五、总结性能监控从单次的 Lighthouse 检查演进为持续的过程监控核心价值在于趋势分析和问题预警。搭建链路可以归纳为四个部分Web Vitals 自定义指标采集、sendBeacon 上报、InfluxDB 时序存储、Grafana 可视化与告警。关键的工程细节包括使用 sendBeacon 保证卸载时的数据不丢失、在上报网关中做数据验证和限流、在 Grafana 中为核心指标配置 P75/P95 而非平均值。通过这些措施可以建立起从感觉变慢了到数据显示 LCP 从 2.1s 恶化到 3.4s的精确度量闭环。