前端 RUM:真实用户性能监测
「网站加载快吗?」这个问题,实验室(Lighthouse、压测)回答不了真实答案——用户可能在弱网、地铁、安卓千元机在后台还挂着几十个 App。性能是分用户、分设备、分网络、分地区的,只有从真实用户浏览器里采集到的数据,才反映真实体验。
这就是 RUM(Real User Monitoring,真实用户监控)——从真实用户的页面里采集性能与错误数据,用「真实发生」代替「假设场景」。
一、什么是 RUM
RUM 在页面里嵌入一段脚本,等页面真实加载时,用浏览器的 Performance API 采集每一个真实用户的加载、交互、错误数据,异步上报到服务端做聚合分析。
它的反面是合成监控(Synthetic Monitoring):用脚本在固定环境里模拟访问,测的是「理想情况」。
| RUM(真实用户监控) | 合成监控 | |
|---|---|---|
| 数据来源 | 真实用户的浏览器 | 模拟脚本(Puppeteer 等) |
| 覆盖 | 全部用户、各种设备/网络 | 固定的几种环境 |
| 优点 | 反映真实体验、能发现长尾问题 | 稳定可控、可复现、适合回归 |
| 缺点 | 采样有噪声、难复现 | 测不出真实用户的痛 |
| 回答的问题 | 「用户实际体感如何」 | 「这条路本身通不通、快不快」 |
两者是互补的:合成监控保证「基线不退化」,RUM 告诉你「真实用户正在经历什么」。本文聚焦 RUM。
二、采什么:核心指标
三大核心 Web Vitals
| 指标 | 全称 | 含义 | 好/差阈值 |
|---|---|---|---|
| LCP | Largest Contentful Paint | 最大内容渲染时间 | 好 ≤ 2.5s,差 > 4s |
| INP | Interaction to Next Paint | 交互到下一次绘制的延迟(2024 年取代 FID) | 好 ≤ 200ms,差 > 500ms |
| CLS | Cumulative 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 数据量可以降一个量级。
五、怎么用:从原始数据到可行动结论
采集到的原始数据不能直接看,要经过聚合和分析:
- 分位值代替平均值。平均数会被极值拉偏,监控都看 p75 / p90 / p95 / p99——「50% 用户 1.2s,95% 用户 3.8s」比「平均 1.9s」信息量大得多,尤其能暴露长尾用户。
- 按维度切片(Segmentation)。同样的指标拆开看才有意义:按页面、按地区、按浏览器、按设备、按网络类型。「移动端弱网用户 LCP 4.5s」 比「整体 LCP 2.1s」更能定位问题。
- 关联错误。性能恶化往往伴随错误率上升,把性能指标和 JS 错误、接口错误放同一张图看。
- 定 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 补上的是前端性能里最后、也最真实的那一块拼图。
