前端监控简介

性能数据上报

掌握用 Performance API 和 web-vitals 库采集性能指标,并上报到服务端的基本流程。

🎯 引言

埋点告诉我们用户在页面上「做了什么」,但回答不了「页面快不快」:你的开发机秒开,不代表用户在旧手机加弱网下也秒开。性能数据上报就是把真实用户设备上的性能指标采集回来、发到服务端汇总,这种思路叫 RUM(Real User Monitoring,真实用户监控)

学完本篇,你将能够:

  • 用浏览器内置的 Performance API 读取导航耗时和绘制时间点。
  • web-vitals 库采集 LCP、INP、CLS 三大核心指标。
  • sendBeacon 把数据上报到服务端,串起「采集 → 上报」的完整流程。

🧱 Performance API 采集指标

浏览器内置了 Performance API,从页面加载开始就记录各阶段耗时,用 performance.getEntriesByType(类型) 按类型读取。常用的一类是 navigation(导航) 记录:

timing.js
// 等页面加载完成后再读取,数据才完整
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(绘制) 记录:

timing.js
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 就是指标数值:

vitals.js
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)));
这些回调不是页面一加载就触发。像 LCP 这种会不断更新的指标,库会等到页面切到后台或关闭时才上报最终值。本地测试时切一下浏览器标签页再切回来,才能在控制台看到输出,这是正常行为。

🧱 上报到服务端

上报直接用上一篇学过的 sendBeacon:页面关闭时也由浏览器保证发出,正好匹配性能指标「页面关闭时才出最终值」的特点。采集加上报的完整最小示例:

vitals.js
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 后入库、按页面和时间聚合即可,存储和看板属于后端范畴,前端把数据采准、发出去就完成了职责。

Performance API 读到的 FCP、服务器响应耗时也可以放进同一个 report 函数一起上报,字段结构保持一致即可。

🧾 小节总结

  • 性能数据上报回答「真实用户端页面快不快」,开发机上的体感判断不出来。
  • Performance API 是浏览器内置能力:navigation 记录看加载各阶段耗时,paint 记录看白屏结束时刻(FCP)。
  • web-vitals 把 LCP、INP、CLS 的采集封装成 onLCPonINPonCLS,回调里的 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 这类监控平台,几行配置就把错误、性能监控一站式接入。