跳到主要内容

前端 RUM:真实用户性能监测

· 阅读需 7 分钟

「网站加载快吗?」这个问题,实验室(Lighthouse、压测)回答不了真实答案——用户可能在弱网、地铁、安卓千元机在后台还挂着几十个 App。性能是分用户、分设备、分网络、分地区的,只有从真实用户浏览器里采集到的数据,才反映真实体验。

这就是 RUM(Real User Monitoring,真实用户监控)——从真实用户的页面里采集性能与错误数据,用「真实发生」代替「假设场景」。

一、什么是 RUM

RUM 在页面里嵌入一段脚本,等页面真实加载时,用浏览器的 Performance API 采集每一个真实用户的加载、交互、错误数据,异步上报到服务端做聚合分析。

它的反面是合成监控(Synthetic Monitoring):用脚本在固定环境里模拟访问,测的是「理想情况」。

RUM(真实用户监控)合成监控
数据来源真实用户的浏览器模拟脚本(Puppeteer 等)
覆盖全部用户、各种设备/网络固定的几种环境
优点反映真实体验、能发现长尾问题稳定可控、可复现、适合回归
缺点采样有噪声、难复现测不出真实用户的痛
回答的问题「用户实际体感如何」「这条路本身通不通、快不快」

两者是互补的:合成监控保证「基线不退化」,RUM 告诉你「真实用户正在经历什么」。本文聚焦 RUM。

二、采什么:核心指标

三大核心 Web Vitals

指标全称含义好/差阈值
LCPLargest Contentful Paint最大内容渲染时间好 ≤ 2.5s,差 > 4s
INPInteraction to Next Paint交互到下一次绘制的延迟(2024 年取代 FID)好 ≤ 200ms,差 > 500ms
CLSCumulative Layout Shift累积布局偏移好 ≤ 0.1,差 > 0.25

这三个指标对应真实体验的三个维度:加载(LCP)、交互(INP)、视觉稳定(CLS)。

扩展指标

定位问题时,光有三个 Web Vitals 不够,还要采集:

  • TTFB(首字节时间)—— 服务端和网络有多慢
  • FCP(首次内容绘制)—— 白屏到有内容
  • 长任务(Long Tasks) —— 主线程被阻塞超过 50ms,往往是卡顿的元凶
  • 资源加载(JS/CSS/图片/接口)—— 慢在哪一个请求
  • JS 错误 / Promise 异常 —— 线上崩溃

三、怎么采:三个 API 家族 + 错误捕获

采集的核心是 PerformanceObserver——订阅性能条目,异步回调,不会阻塞主线程。

LCP:取最后一次出现的最大内容

new PerformanceObserver((list) => {
const entries = list.getEntries();
const lcp = entries[entries.length - 1]; // LCP 可能被更大的内容更新
report('lcp', { value: lcp.startTime });
}).observe({ type: 'largest-contentful-paint', buffered: true });

buffered: true 表示订阅时已发生的历史条目也一次性回放,保证 PerformanceObserver 注册得晚也能拿到数据。

CLS:累加布局偏移(排除用户主动输入)

let cls = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) cls += entry.value; // 用户点击/输入引发的偏移不算
}
report('cls', { value: cls });
}).observe({ type: 'layout-shift', buffered: true });

INP:取交互中「最慢的一次」

let inp = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.interactionId > 0 && entry.duration > inp) {
inp = entry.duration;
}
}
report('inp', { value: inp });
}).observe({ type: 'event', buffered: true, durationThreshold: 40 });

durationThreshold: 40 让浏览器只上报超过 40ms 的交互(小于 40ms 的交互不需要关注)。

TTFB:读 navigation 条目

const nav = performance.getEntriesByType('navigation')[0];
// TTFB = 从导航开始到收到首个字节(navigation 条目的 startTime 恒为 0)
const ttfb = nav.responseStart - nav.startTime;
report('ttfb', { value: ttfb });

⚠️ 常见误区:TTFB 不是 responseStart - requestStart。那只是「请求发出后,服务器多久开始响应」的纯后端耗时,漏掉了 DNS 解析、TCP 建连、TLS 握手。真正的 TTFB(web.dev / MDN 口径)是 responseStart - navigationStart,现代 API 里即 responseStart - startTime,覆盖「导航开始 → 首字节」的全链路。两者含义不同,别混用。

错误捕获:JS 错误 + Promise 异常

window.addEventListener('error', (e) => {
report('error', {
type: 'js',
message: e.message,
stack: e.error?.stack,
source: e.filename,
line: e.lineno,
});
});

window.addEventListener('unhandledrejection', (e) => {
report('error', { type: 'unhandledrejection', reason: String(e.reason) });
});

四、怎么报:别让监控拖慢页面

监控脚本自己也不能拖垮性能,上报策略很关键:

用 sendBeacon,页面关了也能送出去

navigator.sendBeacon 是专为「页面即将卸载时上报」设计的——它不阻塞页面关闭,浏览器会在后台把数据送走:

function report(type, data) {
const body = JSON.stringify({
type,
data,
time: Date.now(),
page: location.pathname,
ua: navigator.userAgent,
});
if (navigator.sendBeacon) {
navigator.sendBeacon('/rum', body);
} else {
fetch('/rum', { method: 'POST', body, keepalive: true }); // 兜底
}
}

在页面隐藏/卸载时兜底上报

部分指标(如 INP、CLS)是「攒着等最终值」的,最好在页面快离开时统一上报一次:

document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') flush();
});
window.addEventListener('pagehide', () => flush());

采样率

监控脚本和数据传输本身有成本,用户量大的站点不必采集 100%。按页面/用户采样(如 10%),数据够统计即可:

const SAMPLE_RATE = 0.1;
if (Math.random() > SAMPLE_RATE) return; // 不采集这个用户

批量 + 压缩

把多条记录攒成一个数组,定时批量 POST,减少请求数;字段用短键名,body 数据量可以降一个量级。

五、怎么用:从原始数据到可行动结论

采集到的原始数据不能直接看,要经过聚合和分析:

  1. 分位值代替平均值。平均数会被极值拉偏,监控都看 p75 / p90 / p95 / p99——「50% 用户 1.2s,95% 用户 3.8s」比「平均 1.9s」信息量大得多,尤其能暴露长尾用户。
  2. 按维度切片(Segmentation)。同样的指标拆开看才有意义:按页面、按地区、按浏览器、按设备、按网络类型。「移动端弱网用户 LCP 4.5s」 比「整体 LCP 2.1s」更能定位问题。
  3. 关联错误。性能恶化往往伴随错误率上升,把性能指标和 JS 错误、接口错误放同一张图看。
  4. 定 SLO。给核心指标定目标(如 LCP p75 ≤ 2.5s),配告警,超过阈值自动提醒——监控的终点不是「看到数据」,而是「数据异常时有人知道」。

六、自建还是用现成的

  • 自建:写个 SDK(就是上面这些代码)+ 上报接口 + 存储(ClickHouse 这类列式存储适合时序聚合)。成本是存储、查询、聚合、看板都要自己搭,适合对数据主权有要求的团队。
  • 商业方案:Sentry、Datadog RUM、Google 的体验报告(CrUX)、国内常用阿里 ARMS 前端监控、字节跳动的 Aegis 等。开箱即用,自带看板、告警、错误堆栈还原。

自建的核心价值在于「采集是自己的」,数据可随意切片、可联动业务;商业方案的价值在于「分析是自己的」,开箱即用、维护零成本。小团队起步先用商业方案,等数据量和个人定制需求上来了再考虑自建。

总结

RUM 回答的不是「页面设计得怎么样」,而是「用户真实用起来快不快、稳不稳」。它的实现路径很清晰:

  • PerformanceObserver 采 Web Vitals(LCP / INP / CLS)+ TTFB / 长任务 / 资源,error / unhandledrejection 采错误
  • sendBeacon 异步送走,采样 + 批量控制成本,页面隐藏时兜底
  • :看分位值不看平均数,按页面/地区/设备/网络切片,配 SLO 和告警

从「我在本地测着挺快」到「我知道 95% 用户的实际体感」,RUM 补上的是前端性能里最后、也最真实的那一块拼图。