PerformanceObserver 必须在第一行内联注册,否则无法捕获FCP、LCP等首屏指标;需监听'paint'和'navigation',禁用defer/async/module,用Date.now()打点,上报仅限sendBeacon(),避免DOM操作与高频dataset读取。

PerformanceObserver 必须在 第一行内联注册
移动端混合应用里,FCP、LCP 这类指标一旦错过就永远收不到——WebView 初始化后触发的 paint 事件不会重放,晚注册等于没监控。很多团队把脚本塞进 DOMContentLoaded 回调或用 async 加载,结果线上首屏数据全为空。
正确做法是:一段 ≤1.5KB 的 IIFE,直接写在 最开头,不加任何属性:
<script>
(() => {
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'paint' && entry.name === 'largest-contentful-paint') {
sendToMonitoring({ metric: 'LCP', value: entry.startTime });
}
}
});
observer.observe({ entryTypes: ['paint', 'navigation'] });
})();
</script>- 必须同时监听
'paint'和'navigation':前者捕获渲染指标,后者提供加载上下文(如重定向、TTFB) - 禁用
defer、async、type="module"(除非确认 WebView 支持且无 top-level await) - 不要在
onload或setTimeout里调用observe(),此时事件早已丢弃
避开 iOS Safari 和低端 Android 的兼容陷阱
iOS Safari 对 performance.measure() 支持不稳定,部分机型会直接忽略打点;Chrome 120+ 已将 performance.timing 标为 deprecated,但混合应用常需兼容 iOS 14.5 以下 WebKit 或企业内网 WebView,这时 navigationStart 和 loadEventEnd 仍是唯一可用的原始时间戳来源。
- 打点统一用
Date.now(),比如在 JS bundle 执行起始处const startAt = Date.now() - 计算首屏耗时别依赖
loadEventEnd - navigationStart,它包含 JS 执行时间,应改用first-contentful-paintstartTime - 检测
navigator.hardwareConcurrency < 2时,跳过longtask监听,只保'paint'+'navigation' - 避免调用
performance.memory:部分 Android WebView 会因此强制 GC,主线程卡顿明显
上报必须用 sendBeacon(),且禁止回调中触碰 DOM
混合应用的 WebView 环境里,同步 XHR 或 fetch 会阻塞页面卸载,导致监控数据丢失;而 console.log 在 DevTools 开启时序列化开销极大,实测拖慢 FCP 达 80ms。
立即学习“前端免费学习笔记(深入)”;
- 上报唯一合规方式:
navigator.sendBeacon('/log', payload),异步、不阻塞、保证送达 - 禁止在
observe回调里执行document.querySelector、getBoundingClientRect()或任何触发 layout 的操作 - 所有数据必须在 entry 到达时立即提取并缓存,比如
entry.startTime、entry.duration、entry.nextHopProtocol - SPA 场景要过滤伪
navigation条目:只保留entry.initiatorType === 'navigation',排除 fetch/XHR 触发的干扰项
data-* 属性高频读写正在悄悄拖慢滚动和动画帧
混合应用里常见滚动监听中反复读取 dataset,比如判断列表项状态:el.dataset.loaded === 'true'。每次访问都触发 DOM 查找和连字符→驼峰转换,每帧执行几十次就会吃掉大量主线程时间。
- 安全做法:首次读取后缓存到变量或组件实例属性,如
this.loadedMap.set(el, el.dataset.loaded) - 危险模式:
for (let el of list) { if (el.dataset.status === 'active') {...} }—— 循环内重复走 DOM 查找 - 单个元素 data-* 属性建议 ≤5 个,值长度别超 256 字符;严禁塞 JSON 字符串,
outerHTML序列化会慢 3 倍以上 - SSR 场景更敏感:服务端模板注入大量
data-,首屏 HTML 体积增大,TTFB 和解析时间直线上升
真正卡顿往往不出现在 Network 面板里,而在滚动监听回调的 getAttribute 调用频次和 querySelectorAll('[data-action]') 的匹配耗时上——这些细节在低端 Android WebView 中会被放大数倍。



















