页面返回比前进快,但我的监控数据全乱了——bfcache 把性能指标偷偷重置了,我把这件事说清楚了
做过真实用户监控(RUM)的同学,大概都遇到过这种情况:用户从列表页点进详情页,再点返回,页面几乎是瞬间加载的——但你的 LCP 指标却显示这次访问的 LCP 比首次访问还差,INP 也莫名其妙地跳了一下。怎么回事?不是你的代码变了,是浏览器把整个页面的性能数据重置了,而 bfcache 正在背后悄悄做这件事。
bfcache(back/forward cache)是什么?简单说,就是浏览器在用户点击「后退」或「前进」时,把当前页面整个快照存进内存,而不是销毁掉。如果用户很快返回,浏览器直接从内存恢复,连网络请求都不用发。这对用户来说是好事——页面返回几乎是即时的。但对前端性能监控来说,这是一道容易被忽略的坑。
bfcache 恢复页面时,性能数据会全部重置
当页面从 bfcache 恢复时,浏览器并不是简单地把页面重新显示出来。它会重置几乎所有的性能相关状态,包括 Largest Contentful Paint(LCP)的记录、Interaction to Next Paint(INP)的起始时间、First Input Delay(FID)的计时器,以及 PerformanceObserver 已经观察过的条目缓冲区。换句话说,浏览器把这次「返回」当成了一次新的页面访问,但又不是一次完整的重新加载——它只重置了指标,没重新触发那些「首次加载」才会走的优化路径。
举个例子:用户在列表页看到 LCP 是文章封面图(2.1秒),点进详情页,再返回列表页。从用户感受来说,列表页几乎是瞬间出现的。但如果你在页面恢复时重新测量 LCP,浏览器会重新触发一次新的 LCP 记录——而这次 LCP 的候选元素,可能变成了列表里第一个文字标题,加载更快(0.3秒),但你的监控数据就会出现一个异常的「快」值。反过来,如果 bfcache 恢复时页面某些资源已经不在内存里,LCP 反而会变差。这种数据的跳变,如果没有提前处理,会让性能报告的可信度大打折扣。
pageshow 事件里的 persisted 才是关键
浏览器提供了 pageshow 事件来告诉你页面是从哪来的。当 event.persisted 为 true 时,说明页面是从 bfcache 恢复的,而不是正常加载。这个判断是性能监控里最重要的一行代码:
window.addEventListener(‘pageshow’, (event) => {
if (event.persisted) {
// 页面从 bfcache 恢复,性能指标需要特殊处理
resetPerformanceMetrics();
}
});
问题在于,很多监控 SDK 并没有正确处理这个分支。常见的错误做法有两种:第一种是完全忽略 persisted 标志,把 bfcache 恢复当成正常页面访问处理,导致 LCP/FID/CLS 等指标出现无法解释的跳变。第二种是粗暴地在 bfcache 恢复时把指标全清零,这样会导致数据丢失,尤其是当你在 bfcache 恢复前后需要追踪某些关键操作时。
正确的处理方式:指标分类处理
bfcache 恢复后的性能数据处理,应该分三种情况来考虑。
第一种是 LCP。LCP 的值在 bfcache 恢复后会重新计算,但候选元素可能和首次加载不同。推荐的做法是在 bfcache 恢复时,把 LCP 的记录标记为「bfcache 恢复场景」,单独统计,不和正常加载混在一起。如果你的 LCP 目标是 2.5 秒,bfcache 恢复场景下达到 0.8 秒,这不代表你的优化生效了——只是说明缓存命中了。
第二种是 INP。INP 追踪的是用户交互到下一帧渲染的延迟。当页面从 bfcache 恢复时,浏览器的输入处理会重新激活,但如果用户在恢复的瞬间做了交互,INP 的计算起点并不是页面加载时间,而是恢复后的某个时间点。这会导致 INP 值看起来很低,但实际上是测量口径变了,不是真实的性能提升。
第三种是 CLS。CLS 在 bfcache 恢复时会被重置,但如果页面上有动态内容(比如广告、推荐列表)在恢复后立即渲染,CLS 会在恢复后重新累积。这里有个容易被忽略的坑:bfcache 恢复的页面,其动态内容状态可能和进入缓存时不一样(比如用户已经登录,或者推荐列表已更新),如果你的 CLS 累积是从恢复点开始计算的,数据会忽高忽低。
Chrome 的 Page Lifecycle API 给了更多控制力
在 Chromium 内核的浏览器里,还有一个更精细的事件:freeze 和 resume。freeze 在页面进入 bfcache 之前触发,resume 在页面从 bfcache 恢复后触发。利用这两个事件,你可以在 freeze 时保存页面的性能上下文状态,在 resume 时决定是复用还是重新初始化:
document.addEventListener(‘freeze’, () => {
// 保存当前状态,比如滚动位置、表单数据
savePageState();
});
document.addEventListener(‘resume’, () => {
// 恢复时决定是否重置性能监控
if (shouldResetMetrics()) {
performanceMetrics.reset();
}
restorePageState();
});
给监控方案的下一步
如果你在用 Web Vitals 官方库(web-vitals)做监控,它已经在一定程度上处理了 bfcache 的情况,但并不是所有场景都覆盖。更稳妥的做法是:先确认你的监控 SDK 是否监听了 pageshow 的 persisted 标志;如果没有,这可能是你数据里那部分「无法解释的异常值」的来源。其次,把 bfcache 恢复的访问单独归类,不参与正常 Lighthouse 评分计算,只做描述性统计。最后,如果你发现 bfcache 恢复后的 LCP 异常好或异常差,先检查是不是这个原因——大多数时候,这不是你的代码在退化,是浏览器缓存策略在帮忙或者在给你挖坑。
下次看到性能报告中那些「不符合直觉」的 LCP 或 INP 值,先查一下 pageshow event.persisted——大概率是 bfcache 在背后动了数据。
评论区
登录后可评论。