Web 性能监控 RUM:从 synthetic 到真实用户
合成监控告诉你「能不能跑」,真实用户监控告诉你「跑得怎么样」。把两者结合才是性能优化的起点。本文给出 RUM 采集、上报、采样、归因的一整套工程模板。
为什么仅有 synthetic 不够
Synthetic 监控(也叫 robot monitoring)通过一台中心机房的服务器模拟用户访问。它的好处是「稳定可控」,坏处是「不代表真实网络」。
RUM(Real User Monitoring)则反之。它通过在真实用户的浏览器里采集 Navigation Timing、Resource Timing、Long Tasks、Layout Shift 等数据,告诉开发者「用户在你的网站真实体验到的时间、错误、设备分布」。这是性能优化必须的视角。
RUM 与 synthetic 不是替代关系,是叠加关系。一个负责「预警」,一个负责「归因」。
RUM 上报的核心字段
一份合格的 RUM 数据应当包含:
- 页 URL / 路由:用于分页面聚合。
- 设备 / 网络:device pixel ratio、effectiveType、downlink。
- 关键时点:FP、FCP、LCP、TTFB、DCL、Load。
- 资源慢列表:超过阈值的 URL + 耗时。
- 错误栈:JS error、unhandledrejection。
- 用户标识:脱敏后的用户 ID 或会话 ID,用于关联。
上报方式:打点 vs 长连接
RUM 数据量很大(每个会话平均 5–20 个事件),不能直接落到主业务日志。常见做法:
- 打点 + beacon:
navigator.sendBeacon在 unload 时提交,浏览器保证送达,但只支持 POST。 - fetch keepalive:在页面销毁前用 keep-alive fetch 上报。
- 本地聚合 + Image 1×1:把数据 base64 进图,缺点是长度有限。
// 典型 sendBeacon 用法
const report = (event) => {
const url = 'https://rum.yisu.app/api/beacon';
const blob = new Blob([JSON.stringify(event)], { type: 'application/json' });
navigator.sendBeacon(url, blob);
};
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
report({ type: 'page_hide', url: location.href, t: Date.now() });
}
});
采样策略:不是越多越好
全量采集很快会把上报链路压垮。建议按场景分层:
| 场景 | 采样率 | 原因 |
|---|---|---|
| 关键页面(首页 / 详情 / 支付) | 100% | 业务价值高 |
| 次要页面(帮助中心) | 1% | 优先级低 |
| 慢用户(P75 之后) | 100% | 长尾体验 |
| 异常会话 | 100% | debugger |
我们用 session_id 做布隆过滤,对命中「最近 24h 内未上报」的会话保留更多采集预算。
性能数据的归因:从「哪里慢」到「为什么慢」
只有指标没有归因,工程师是没法动手的。常见归因路径:
- 慢 URL → 服务器 trace → 慢 SQL → DB 索引问题
- 慢资源 → CDN 节点 → 拉取 URL 异常
- JS 错误 → stack trace → 业务代码行
- Long Task → 触发动作 → 同步 JS 代码段
把这套归因链路跟 Trace 系统打通,是把 RUM「真的有用」的关键一步。
与服务端 APM 的对接
纯前端 RUM 只能告诉你「很慢」,但不能告诉「谁慢」。所以必须把它和服务端 APM 串联。常见做法:
- 在请求头注入
X-Trace-Id,RUM 把它和 trace 关联。 - 把 trace_id 上报给 RUM 后端,与服务端采样打通。
- 聚合:用户 ID + 时间窗口 + trace_id 三个维度关联。
Core Web Vitals:当下最被广泛采用的指标
LCP、INP、CLS 是 Google 主推的三大核心指标。它们各自的意义是:
| 指标 | 衡量 | 建议 |
|---|---|---|
| LCP | 最大内容渲染时间 | < 2.5s 良好 |
| INP | 下一次交互延迟(FID 升级版) | < 200ms 良好 |
| CLS | 累计布局偏移 | < 0.1 良好 |
用 PerformanceObserver 直接在前端采集:
const lcpObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('LCP candidate:', entry.startTime, entry.element);
}
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
常见坑与避雷
- SPA 路由切换:切换时不会自动触发 LCP,需要手动
disconnect + observe。 - iFrame 子页面:PerformanceObserver 默认不会下钻。
- Worker 任务:跨 Worker 的 Long Task 拆分后大数可能被合并。
- WebView 内部:安卓 WebView 的 PerformanceObserver 实现差异大。
几个真实数据带来的决策
我们去年做了两个基于 RUM 的真实动作:
- 通过 LCP 75 分位的飙升发现一个新上线的 Banner 拖慢了首屏,回退后分数回到正常。
- 通过 INP 75 分位的分布发现安卓某版本机型的点击响应 > 600ms,最后定位到第三方埋点 SDK。
这些发现都不是合成监控能告诉你的。只有真实用户数据才能让性能和「真实世界问题」对上号。
给团队的几个建议
- 把 LCP、INP、CLS 作为发布门禁的三项指标。
- 采集与服务端 APM、Trace 联动,避免信息孤岛。
- 上报用 sendBeacon,避免丢包。
- 数据保留严格分层:聚合数据 90 天,原始采样 7 天。
RUM 不是一个「装上就好」的工具。它是一套持续运营的工程能力,做好它会让你的产品比同行更早知道哪里「卡」。