用户说页面卡,我盯了三年 Lighthouse——今天发现浏览器自己会测三个指标,真实数据直接进了监控台
配了三年性能监控,每次用户说「页面卡」,我只能去 Lighthouse 抓个分——但那个分数是实验室数据,跟真实用户一点关系没有。直到今年我发现 Chrome 自己就塞了三个 API,把真实用户性能数据的采集彻底原生化了。
这三个东西分别是:LoAF(Long Animation Frame)、content-visibility、以及 bfcache 下的指标行为。不夸张地说,把这三个搞定了,你就不用再靠猜测去猜性能问题出在哪。
LoAF:终于知道是哪一行代码把帧卡住了
以前说「页面卡」,你只能看 Total Blocking Time(TBT)——一个笼统的、把所有长任务混在一起的数字。你知道有阻塞,但不知道是谁干的。
LoAF(Long Animation Frame Timing)把这个黑箱拆开了。它专门记录「哪一帧超过 50ms」,并且把那帧里所有耗时超过 50ms 的脚本全部归因出来,包括脚本名称、调用栈、执行时长。
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.name → 帧来源(default/navigate/update)
// entry.duration → 这一帧总耗时(超过50ms就是LoAF)
// entry.blockingDuration → 阻塞主线程的时长
// entry.scripts[] → 这一帧里执行的脚本详情
if (entry.blockingDuration > 100) {
console.log(`LoAF detected: ${entry.duration.toFixed(0)}ms blocking`);
entry.scripts.forEach(s => {
console.log(` Script: ${s.name} (${s.duration.toFixed(0)}ms)`);
});
}
}
});
observer.observe({ type: "long-animation-frame", buffered: true });
这意味着你终于可以精准定位:不是「JS 跑太久了」,而是「这个第三方监控脚本的某个函数占了你 340ms」。配合 Chrome DevTools 的 Performance 面板,你能拿到脚本的具体 URL 和函数名,直接去跟供应商battle。
LoAF 在 Chrome 123+ 支持,已进入 Baseline 2024,Firefox 和 Safari 也在跟进。
content-visibility: auto:让浏览器自己跳过屏幕外渲染
以前做长列表优化,你要手动判断元素是否在视口内,然后手动加上 display: none 或者用 Intersection Observer。但这个方案有两个问题:第一,你要自己写判断逻辑;第二,当你把元素从 DOM 里隐藏,它仍然占着渲染计算资源。
content-visibility: auto 让浏览器自己决定「这个内容现在要不要渲染」——原理跟虚拟列表类似,但它是浏览器原生支持的,你只需要一行 CSS。
.feed-item {
content-visibility: auto;
contain-intrinsic-size: 0 120px; /* 预估高度,防止滚动条跳变 */
}
实测数据:Reddit 把这项技术用到他们的信息流后,初始加载渲染时间提升了 7 倍。不是 7%,是 7 倍。
原理很简单:浏览器会跳过屏幕外元素的布局和绘制,直到它们接近视口才真正渲染。这个「接近」的距离可以通过 contain-intrinsic-size 提前告知浏览器,否则它会把未渲染元素的高度算成 0,导致滚动条在你滚动时突然跳长。
配合 contain 属性使用效果更好:
.card {
content-visibility: auto;
contain-intrinsic-size: 0 200px;
contain: layout style;
}
contain: layout style 进一步隔离了这个元素的布局和样式计算对外部的影响,等于给浏览器上了双保险。
bfcache:页面返回时,你的指标可能偷偷归零了
这个问题藏得很深,但一旦知道就再也忘不掉。
当用户点击浏览器后退键回到你的页面,Chrome 会从 bfcache(Back-Forward Cache)恢复页面——这个过程极快,但代价是:LCP、INP、CLS 这些性能指标会被重置,重新从头开始采集。
也就是说,如果你用 performance.getEntriesByType("largest-contentful-paint") 拿 LCP,在 bfcache 恢复后你会得到一个新的、可能是 0 的值,而不是用户真正看到的「页面恢复到可视状态」的耗时。
正确做法是用 pageshow 事件的 event.persisted 属性判断这次页面展示是否来自 bfcache:
window.addEventListener("pageshow", (event) => {
if (event.persisted) {
// 来自 bfcache 恢复,指标需特殊处理
// 可以选择不上报此次 LCP,或者标记为 bfcache 场景
reportMetric("lcp", null, { fromBfcache: true });
reportMetric("inp", null, { fromBfcache: true });
reportMetric("cls", null, { fromBfcache: true });
}
});
Google 自己的 web.dev 文档已经把这个列为生产环境必做项。如果你发现你的性能仪表盘里有一批 LCP 数据异常低(接近 0 或几十毫秒),大概率就是因为 bfcache 场景被误采集了。
三个 API 合起来是什么
LoAF 告诉你哪帧卡、是谁卡的;content-visibility 让你用一行 CSS 换几倍渲染提速;bfcache 知识让你别把错误的数据上报上去。
这三件事加起来,等于你终于可以在生产环境里精准地「看见」性能问题了——不是靠猜测,不是靠 Lighthouse 的实验室数据,而是真实用户的真实帧数据。
下一步建议:从 content-visibility: auto 开始,最容易出效果;LoAF 配合 PerformanceObserver 埋点,能直接定位慢脚本;最后检查你的监控 SDK 有没有处理 bfcache 场景。这三件事今天就能动手。
评论区
登录后可评论。