一粟 · yisu.app
· 12 min read
监控

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 个事件),不能直接落到主业务日志。常见做法:

  • 打点 + beaconnavigator.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 内未上报」的会话保留更多采集预算。

性能数据的归因:从「哪里慢」到「为什么慢」

只有指标没有归因,工程师是没法动手的。常见归因路径:

  1. 慢 URL → 服务器 trace → 慢 SQL → DB 索引问题
  2. 慢资源 → CDN 节点 → 拉取 URL 异常
  3. JS 错误 → stack trace → 业务代码行
  4. 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 的真实动作:

  1. 通过 LCP 75 分位的飙升发现一个新上线的 Banner 拖慢了首屏,回退后分数回到正常。
  2. 通过 INP 75 分位的分布发现安卓某版本机型的点击响应 > 600ms,最后定位到第三方埋点 SDK。

这些发现都不是合成监控能告诉你的。只有真实用户数据才能让性能和「真实世界问题」对上号。

给团队的几个建议

  • 把 LCP、INP、CLS 作为发布门禁的三项指标。
  • 采集与服务端 APM、Trace 联动,避免信息孤岛。
  • 上报用 sendBeacon,避免丢包。
  • 数据保留严格分层:聚合数据 90 天,原始采样 7 天。

RUM 不是一个「装上就好」的工具。它是一套持续运营的工程能力,做好它会让你的产品比同行更早知道哪里「卡」。