前端数据埋点(7):Web Vitals——页面慢不慢,不是再包一层 fetch

上一篇:web-vitals------三个数怎么算出来

相关:一次点击如何变成一条埋点 · 上报

系列:前端数据埋点 · 第 7 篇

第 6 篇已经会订 onLCP / onINP / onCLS,也知道 Metric 里有 value 和最大元素。本篇接上去:数还会变,何时才变成一条埋点,切标签和后退缓存不是一回事。代码取自 Sentry JavaScript SDK 对 web-vitals 的接入,讲机制,不是产品接入。


先说结论

页还开着的时候,三个数还能变。LCP 可能先是标题 800ms,大图上来变成 2.4s。每次变都当一条 EVENT 发出去,同一页会留下一串越来越大的值,服务端再聚合也分不清这一页到底有多慢。

做法和第 1 篇的 DURATION 一样:过程中只改内存里的当前值,页被切走、关掉、或站内跳到下一页时,发一次终值。关页那次 POST 仍走第 4 篇的原生 fetch + keepalive

切到别的标签再切回来,只是 hiddenvisible,同一套数,不会重算。后退缓存是另一件事:点链接离开文档后,后退时旧页从内存解冻,库会再给一套很快的数。这份实现丢掉,不是把当前 vitals 清零。

一句话:Web Vitals 是这一页体感如何的终值,不是每次绘制、每次点击的埋点。


1. 三层里,本篇只谈最后一层

复制代码
浏览器出原料(PerformanceObserver)
  → 库收成 Metric(第 6 篇的 onLCP / onINP / onCLS)
  → SDK 只写内存;页变成 hidden / 离开时发一次终值

第 6 篇把中间一层写完了。SDK 订的就是那三个函数。Sentry 还给 LCP、CLS 开了 reportAllChanges: true,好刷新内存。那是订阅方的选择,不是库的默认。

没有 Performance API 就放弃,不要手搓假 LCP。终值不要当 click 进行为栈。


2. 概念词典

第 6 篇的 MetricratingreportAllChanges 不再重复。本篇只用这几个:

是什么 解决什么
终值 这一页可以定稿时的那一个数 中间值发出去,聚合会乱
后退缓存(bfcache) 离开这个文档后,浏览器把整页冻在内存里,点后退或前进时解冻,JS 不重跑 和切到别的标签不是一回事
软导航 文档没换,路由换了(SPA) 浏览器默认的 LCP 只认硬加载那一次

3. 回调密,不等于已经上报

web-vitals 默认会等标签页进后台,再回调一次。SDK 给 LCP、CLS 开了 reportAllChanges: true:值一变就通知,方便改内存里的当前值。通知不等于 POST。

摘自 packages/browser-utils/src/instrumentation/performanceObserver.ts

240:251:packages/browser-utils/src/instrumentation/performanceObserver.ts 复制代码
function instrumentCls(): StopListening {
  return onCLS(
    withoutBfcache(metric => {
      triggerHandlers('cls', {
        metric,
      });
      _previousCls = metric;
    }),
    // We want the callback to be called whenever the CLS value updates.
    // By default, the callback is only called when the tab goes to the background.
    { reportAllChanges: true },
  );
}

LCP 同样。信封是另一处触发的:页变成 hidden(切走、关掉、合上笔记本),或者站内软跳转开始。后者只收这一次进入页的 LCP,下一页是新的一页。

摘自 packages/browser-utils/src/web-vitals/reportEvents.ts

34:45:packages/browser-utils/src/web-vitals/reportEvents.ts 复制代码
  onHidden(() => {
    _runCollectorCallbackOnce('pagehide');
  });

  const unsubscribeStartNavigation = client.on('beforeStartNavigationSpan', (_, options) => {
    // we only want to collect LCP if we actually navigate. Redirects should be ignored.
    if (!options?.isRedirect) {
      _runCollectorCallbackOnce('navigation');
      unsubscribeStartNavigation();
      unsubscribeAfterStartPageLoadSpan();
    }
  });

_runCollectorCallbackOnce 保证同一页只发一次。落到埋点上,就是 track('DURATION', { trackId: 'checkout_lcp', value }),不是每 200ms 一条。

INP 没有开 reportAllChanges。库照旧在页进后台时给出终值,Sentry 在那次 onINP 里发出去。对你来说仍是离开或隐藏时那一下,不是每次点击一条。

关页那次要带 keepalive。最后一跳的 LCP 若常丢,统计会偏向那些一直挂着、没关页的人。

结算页上实际是这样走的:0.8s 标题成为 LCP,内存写成 800,不发;2.4s 主图更大,改成 2400,还不发;用户点了三次,最差一次 350ms,INP 停在 350。直到切走、关掉或路由离开,才把这三个终值发出去。

接到自己的管线上:

js 复制代码
onLCP((metric) => {
  if (metric.navigationType === 'back-forward-cache') return
  current.lcp = metric
})

// 页 hidden / 离开时
track('DURATION', {
  trackId: 'checkout_lcp',
  value: current.lcp.value,
  rating: current.lcp.rating,
})

4. 切到别的标签,不是后退缓存

页从看不见变回看得见,不等于发生了后退缓存。

切到旁边的标签再切回来,文档还在,只是 hidden 过一次。终值在第一次 hidden 时已经发过,回来不会重算,也不会再 POST。

点了站内链接离开这个文档,再按浏览器后退,有两种结果。一种是整页重新加载,旧文档没了,这是新的一页,vitals 正常再测,Navigation Timing 上的类型是 back_forward。另一种是旧页还冻在内存里,JS 没重跑,整页解冻------这才是 bfcache

名字里的 back-forward,指浏览器给后退、前进留的整页快照。切标签不走这条路。软导航也不是 bfcache:软导航是没离开这个文档。

navigationType === 'back-forward-cache' 不是 Sentry 算的,更不是 performance.getEntriesByType('navigation')[0].type。那条 entry 没有这个取值。第 6 篇写过:库听的是 pageshowevent.persisted === true 才表示解冻,然后新建一套 Metric 再回调。

离开文档时,终值多半已经发出去了,「这一页发过」也记下了。解冻后 JS 堆还在,旧数字还在,并没有被清零。库只是又塞来一套新 id 的数。画面早就画好了,这套数会偏快。混进这一页的统计,会把真的慢稀释掉。

这份实现看到这个类型就直接返回,不改内存,也不发信封:

231:237:packages/browser-utils/src/instrumentation/performanceObserver.ts 复制代码
function withoutBfcache(callback: (metric: Metric) => void): (metric: Metric) => void {
  return metric => {
    if (metric.navigationType === 'back-forward-cache') {
      return;
    }
    callback(metric);
  };
}

如果以后要单独看「从缓存回来」的体感,再为这个类型建模型。默认丢掉即可。切标签回来没有这套新数,不用处理。

发出去时至少带上:指标名、valuerating、当前路由 name(第 5 篇)、LCP / INP 的 DOM 路径(元素仍从第 6 篇的 metric.entries 取)。vitals 每 PV 最多三条,比满屏 EVENT 稀,采样可以是 1 或略低。错误仍用 1


5. 业务自己的时长:mark / measure

核心三个只覆盖页级体感。支付从点下到出结果,要用自己的时间:

js 复制代码
performance.mark('pay_start')
await doPay()
performance.mark('pay_end')
performance.measure('pay', 'pay_start', 'pay_end')
const entry = performance.getEntriesByName('pay').pop()
track('DURATION', { trackId: 'pay_duration', value: entry.duration })

这是 User Timing,和 LCP 不是同一个观察者。浏览器没有 performance 就不要发假 0。也不要把 pay_duration 叫成 LCP:一个是交互链路,一个是首屏最大块。


和前作怎么接

补哪一段
(6)web-vitals 三个函数、Metricvalue 和最大元素怎么来
(1)管线 DURATION 这一类时长事件
(3)行为栈 DOM 路径;vitals 终值不要当 click 进栈
(4)上报 关页发出、keepalive、采样
(5)框架钩子 路由 name 挂在 vitals 上,才知道是哪一页慢

到这里,错误、业务点击、页面曝光、体感时长都有了入口。服务端怎么聚合、怎么告警,仍是信封出去之后的事。


你可以从这里带走什么?

  1. 第 6 篇算出的数,页开着时还会变。reportAllChanges 只刷新内存;信封等 hidden 或离开时发一次。
  2. INP 是最差一次交互,不是第一次点击,也不是点击量。
  3. 切标签只是看不见。bfcache 是离开文档后,后退或前进把冻住的页解冻;库会再给一套很快的数,默认丢掉,当前值并没有被清零。
  4. 业务步骤用 performance.measure。关页发出仍要 keepalive

仓库与相关文档


本文是「前端数据埋点」第 7 篇:三个数何时发终值。不展开完整 Tracing span 模型和离线 Performance 日志。

相关推荐
এ慕ོ冬℘゜1 小时前
前端实战:jQuery实现动态表格渲染与状态切换功能
前端·javascript·jquery
jay神2 小时前
【计算机毕业设计】基于Flask+Vue的电影数据可视化与智能推荐系统
前端·vue.js·python·信息可视化·flask·毕业设计·课程设计
FreeTinker8 小时前
HTML本地存储技术解析:localStorage与sessionStorage的原理、差异与应用场景
前端·html
qq_452396238 小时前
第十二篇:《全链路监控与可观测性:前端错误追踪与性能分析》
前端
wangruofeng9 小时前
Phosphor Icons 官网源码拆解:URL 即存储、水波动画与 7362 Star 图标库的架构实践
前端·架构·github
BigTopOne10 小时前
WebRTC Native / Android 开发学习资源整理
前端
excel11 小时前
nuxt 3 升级 nuxt 4 报错之 hasInjectionContext
前端
何以解忧,唯有..11 小时前
LangChain 工具调用(Tool Calling)实战指南
java·前端·langchain
软件聚导航12 小时前
「聚小软 AI 助手」技术升级:从数据库检索到 RAG 知识库问答
前端·数据库·mysql·微信小程序·小程序·ai编程·rag