性能数据上报
🎯 引言
埋点告诉我们用户在页面上「做了什么」,但回答不了「页面快不快」:你的开发机秒开,不代表用户在旧手机加弱网下也秒开。性能数据上报就是把真实用户设备上的性能指标采集回来、发到服务端汇总,这种思路叫 RUM(Real User Monitoring,真实用户监控)。
学完本篇,你将能够:
- 用浏览器内置的 Performance API 读取导航耗时和绘制时间点。
- 用 web-vitals 库采集 LCP、INP、CLS 三大核心指标。
- 用
sendBeacon把数据上报到服务端,串起「采集 → 上报」的完整流程。
🧱 Performance API 采集指标
浏览器内置了 Performance API,从页面加载开始就记录各阶段耗时,用 performance.getEntriesByType(类型) 按类型读取。常用的一类是 navigation(导航) 记录:
// 等页面加载完成后再读取,数据才完整
window.addEventListener('load', () => {
const [nav] = performance.getEntriesByType('navigation');
console.log('服务器响应耗时', Math.round(nav.responseStart - nav.requestStart));
console.log('DOM 解析完成耗时', Math.round(nav.domContentLoadedEventEnd));
console.log('页面完全加载耗时', Math.round(nav.loadEventEnd));
});
nav 上的字段是加载各节点的时间戳(毫秒),两两相减即得阶段耗时。比如 responseStart - requestStart 就是服务器响应耗时,loadEventEnd 则是页面完全加载的总耗时。
另一类常用的是 paint(绘制) 记录:
performance.getEntriesByType('paint').forEach((entry) => {
console.log(entry.name, Math.round(entry.startTime));
// first-paint:屏幕上第一次画出任何像素
// first-contentful-paint:第一次画出文字、图片等实际内容
});
startTime 是「页面打开后第几毫秒」,可直接使用。其中 first-contentful-paint(简称 FCP)代表白屏结束、用户看到内容的时刻。
load 事件触发后再读取 navigation 记录。读早了,loadEventEnd 等字段还是 0,算出来的耗时会是错误的负数或零值。navigation 看加载各阶段耗时、paint 看白屏结束时刻就够了,其余字段用到时再查 MDN 文档。🧱 web-vitals 库
LCP、INP、CLS 三大核心指标靠手写监听很难采准:LCP 随内容绘制不断更新、INP 要跟踪整次访问的所有交互、CLS 要累计每一次布局偏移。Google 官方的 web-vitals 库把这套逻辑封装成了三个函数。指标的概念和合格标准见 性能优化课程 的《性能指标与 Lighthouse》,这里只讲采集和上报。
先安装,以下示例在 Node.js v22 + web-vitals v5.x 下验证:
pnpm add web-vitals
三个函数对应三个指标,回调里的 metric.value 就是指标数值:
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP((metric) => console.log('LCP(毫秒)', Math.round(metric.value)));
onINP((metric) => console.log('INP(毫秒)', Math.round(metric.value)));
onCLS((metric) => console.log('CLS(无单位分数)', metric.value.toFixed(2)));
🧱 上报到服务端
上报直接用上一篇学过的 sendBeacon:页面关闭时也由浏览器保证发出,正好匹配性能指标「页面关闭时才出最终值」的特点。采集加上报的完整最小示例:
import { onLCP, onINP, onCLS } from 'web-vitals';
function report(metric) {
const data = {
name: metric.name, // 指标名:LCP / INP / CLS
value: Math.round(metric.value), // 取整后上报
path: location.pathname, // 哪个页面
};
const body = JSON.stringify(data);
navigator.sendBeacon('/api/monitor/performance', new Blob([body], { type: 'application/json' }));
}
onLCP(report);
onINP(report);
onCLS(report);
path 用于区分页面:不同页面性能差异很大,服务端汇总时要按页面分开统计,混在一起就没有指导意义了。服务端收到 JSON 后入库、按页面和时间聚合即可,存储和看板属于后端范畴,前端把数据采准、发出去就完成了职责。
report 函数一起上报,字段结构保持一致即可。🧾 小节总结
- 性能数据上报回答「真实用户端页面快不快」,开发机上的体感判断不出来。
- Performance API 是浏览器内置能力:
navigation记录看加载各阶段耗时,paint记录看白屏结束时刻(FCP)。 - web-vitals 把 LCP、INP、CLS 的采集封装成
onLCP、onINP、onCLS,回调里的metric.value就是指标数值。 - 指标回调常在页面隐藏或关闭时才触发,所以上报要用页面关闭也不丢数据的
sendBeacon。 - 上报带上
path等上下文,服务端才能按页面维度统计。
❓ 知识问答
Q1:为什么我注册了 onLCP,页面加载完控制台却没有输出?
A:正常行为。LCP 会随内容绘制不断更新,库等页面切到后台或关闭时才回调最终值,本地测试切换一下标签页即可触发。
Q2:上报为什么用 sendBeacon 而不是 fetch?
A:性能指标常在页面关闭那一刻才产生,此时浏览器可能取消进行中的 fetch,导致数据丢失;sendBeacon 由浏览器接管保证发出。详细对比见上一篇《埋点与数据上报》。
Q3:每个用户每次访问都上报,数据量会不会太大?
A:可以采样上报,比如用 Math.random() < 0.1 只让约一成用户执行上报,统计上仍能反映整体情况,比例按访问量调整。
🧪 小练习
练习一:补全下面的代码,用 Performance API 计算「服务器响应耗时」(从发出请求到收到第一个字节),取整后打印:
window.addEventListener('load', () => {
const [nav] = performance.getEntriesByType('navigation');
// 请在这里编写代码:计算并打印服务器响应耗时
});
练习二:给 report 的数据加一个 ua 字段(值为 navigator.userAgent),方便服务端按浏览器区分数据:
function report(metric) {
const data = {
name: metric.name,
value: Math.round(metric.value),
path: location.pathname,
// 请在这里编写代码:补充 ua 字段
};
const body = JSON.stringify(data);
navigator.sendBeacon('/api/monitor/performance', new Blob([body], { type: 'application/json' }));
}
完成后想想:如果想按「移动端 / PC 端」分开统计,除了上报完整的 UA 字符串,有没有更省事的做法?
🎉 恭喜你已经掌握性能数据上报技能啦!下一篇我们看看如何用 Sentry 这类监控平台,几行配置就把错误、性能监控一站式接入。
