核心是用 PerformanceObserver 监听 paint 和 navigation 类型指标,首屏性能捕获需在 head 中尽早初始化;RUM 的 TTFB 需排除服务端渲染与协议干扰;LongTask 比 FPS 更可靠反映卡顿;卸载时用 sendBeacon 发送监控数据。
核心是利用 PerformanceObserver 监听 navigation 和 paint 类型指标,而非依赖 onload 或 DOMContentLoaded——后者无法反映真实用户感知的“内容可见”时间。
navigation 提供 loadEventStart、domComplete 等完整导航生命周期,但注意单页应用(SPA)中后续路由跳转不会触发新 navigation 记录paint 可捕获 first-paint 和 first-contentful-paint(FCP),FCP 是 Lighthouse 和 CrUX 的关键指标,必须用 PerformanceObserver 订阅,performance.getEntriesByType('paint') 在页面后期调用可能漏掉DOMContentLoaded 后才初始化 observer,应放在 内尽早执行,否则错过首屏关键事件const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
sendToMonitoring({ metric: 'FCP', value: entry.startTime });
}
}
});
observer.observe({ entryTypes: ['paint'] });TTFB(Time to First Byte)在 RUM 中不能直接取 performance.getEntriesByType('navigation')[0].responseStart - performance.getEntriesByType('navigation')[0].requestStart 就完事——CDN 缓存、HTTP/2 多路复用、服务端渲染(SSR)或边缘计算都会让这个差值失真。
requestStart 到 responseStart 实际包含服务端模板渲染耗时,不是纯网络延迟requestStart 可能被浏览器合并或延迟调度,导致 TTFB 偏高connectStart 和 secureConnectionStart,判断是否卡在 TLS 握手(尤其 iOS WebKit 对 OCSP stapling 敏感)const nav = performance.getEntriesByType('navigation')[0];
const ttbfRaw = nav.responseStart - nav.requestStart;
const tlsDelay = nav.secureConnectionStart > 0
? nav.connectEnd - nav.secureConnectionStart
: 0;LongTask 监控卡顿比 FPS 更可靠FPS 是推算值,依赖 requestAnimationFrame 回调节拍,而主线程被阻塞时回调根本不会触发,造成“假高帧率”。LongTask 是浏览器原生暴露的 >50ms 的任务记录,直接反映 JS 执行、样式计算、布局等阻塞行为。
longtask 类型即可,无需自行 diff 时间戳;每个 entry.duration 就是实际阻塞时长LongTask,旧版本需回退到 event loop delay 采样(如每 100ms postMessage 检查时间差)duration > 100ms 就可能引发明显掉帧,建议按 50ms / 100ms / 200ms 分档上报,便于定位是偶发 GC 还是持续轮询const lo = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.duration > 100) {
sendToMonitoring({
metric: 'LongTask',
duration: entry.duration,
attribution: entry.attribution
});
}
});
});
lo.observe({ entryTypes: ['longtask'] });用 navigator.sendBeacon() 是唯一稳妥方案。XMLHttpRequest 或 fetch 在 beforeunload 中发起请求,浏览器大概率会中断,尤其在 iOS Safari 和 Chrome 移动端。
sendBeacon() 是异步且不可取消的,即使页面已销毁也会发出请求,但仅支持 POST 且 payload 必须是 ArrayBuffer、Blob 或 FormData
visibilitychange 隐藏时立刻发 beacon,应加 300ms 延迟并检查 document.visibilityState === 'hidden',避免误报后台标签页切换Blob,不能传字符串const data = new Blob([JSON.stringify(performanceMetrics)], {
type: 'application/json'
});
navigator.sendBeacon('/log', data);真实场景里最容易被忽略的是:单页应用中 PerformanceNavigationTiming 不会随路由更新,而开发者常误以为它能反映每次页面切换的性能。必须结合 History API 监听 + 手动打点,或者改用 Navigation Timing Level 2 的 navigation observer 并启用 buffered: true。