点了三年返回键,今天才发现这 20% 的页面根本不是「快」——是浏览器偷偷从内存里捞出来的

你点返回键,页面秒开。你以为是网速快,其实不是。

Chrome 里大约 20% 的返回/前进导航,根本没有走网络请求——浏览器把整个页面状态快照存进了内存,你点返回的时候直接「解冻」,连解析 HTML、带解析 JS 都不用。

这个机制叫 bfcache(Back/Forward Cache)。它不是 HTTP 缓存,不是 Service Worker 缓存,而是浏览器进程里的整页内存快照。速度快到没有 LCP 可量,因为根本没有 Paint 在发生。

但问题来了:你的站点,很可能正在亲手破坏它。


unload 监听器:最大的罪魁祸首

第三方脚本装上监听器的成本极低,但伤害是真实的。Google 的数据显示,有 unload 监听器的页面,bfcache 命中率平均下降 18 个百分点。

unload 原本的意思是「页面即将卸载」,但对浏览器来说这意味着「不能安全地冻结这个页面」,因为 unload 代码预期一定会执行。实际情况更糟:移动端 unload 根本不一定触发,Chrome 优先保 bfcache 而跳过它——所以你装的这个监听器,既不能可靠执行,又把最快的一条路堵死了。

解法很简单:把 unload 换成 pagehidepagehide 在页面进入 bfcache 之前触发,行为覆盖了 unload 的所有合法用途,且不会阻止缓存。

// Bad
window.addEventListener('unload', () => {
  navigator.sendBeacon('/analytics/leave');
});

// Good
window.addEventListener('pagehide', (event) => {
  navigator.sendBeacon('/analytics/leave');
});

Cache-Control: no-store:你以为在防过期,其实在堵高速

另一个高频踩坑点是 no-store 头。很多站点全站默认 no-store 来「保证不返回过期内容」,结果把 bfcache 也一起关了小黑屋。

2025 年 Chrome 做了一轮行为调整,在特定安全条件下允许 no-store 页面进 bfcache,但约束条件多,且是 Chrome 特有。如果你的页面有 Cookie 或认证状态变化,冻结快照会立刻被驱逐。

对大多数内容型页面,Cache-Control: max-age=0, must-revalidate 已经足够保证新鲜度,同时保留了 bfcache 资格。如果你的页面根本没有私人敏感数据,甚至可以直接用 max-age=60 或更短。


WebSocket、IndexedDB:连接不关,缓存不入

打开的 WebSocket 连接、进行中的 IndexedDB 事务、未关闭的 BroadcastChannel——这些在导航发生时如果还处于活跃状态,都会阻止 bfcache。

不是说不能用,而是要在 pagehide 时主动断开,在 pageshow 恢复。

window.addEventListener('pagehide', () => {
  if (event.persisted) {
    // 页面要进 bfcache,主动关闭连接
    myWebSocket?.close();
  }
});

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // 页面从 bfcache 恢复,重新建立
    initWebSocket();
  }
});

怎么知道自己有没有踩坑?两个 API 直接告诉你

Chrome 123+ 内置了 NotRestoredReasons API,可以在代码里直接拿到哪些原因导致你的页面没有走 bfcache:

const nav = performance.getEntriesByType('navigation')[0];
const reasons = nav.notRestoredReasons;
console.log('bfcache 拦截原因:', reasons);

Chrome DevTools 也有图形界面:Application → Back/forward cache → Run Test,能列出所有 blocker 并给出修复建议。

另外一个自检方式:在你的分析脚本里监听 pageshowevent.persisted

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    console.log('bfcache 命中——这次返回是内存恢复,没有网络请求');
  } else {
    console.log('bfcache 未命中——走了完整加载');
  }
});

bfcache 到底影响多大?Yahoo! JAPAN News 的真实数据

Yahoo! JAPAN News 做过一次排查,把移动端的 unload 监听器全部替换为 pagehide,配合缓存头调整,结果:

  • 每次会话 pageview 提升 9%
  • 返回/前进导航的 LCP 大幅下降
  • 用户留存上升,因为返回感觉「秒开」

这个改动几乎没有代码新增,主要是删。删掉一个 unload 监听器、一个 no-store 头,换来 10%~20% 导航路径的零延迟。


下一步:30 分钟排查清单

  1. 打开 DevTools → Application → Back/forward cache → Run Test,看结果
  2. 搜索代码库里所有 addEventListener('unload'——第三方脚本装的最常见
  3. 查 Response Header 里有没有 Cache-Control: no-store,确认是否真的需要
  4. 在 pageshow 里加 event.persisted 判断,让你的分析系统区分「新鲜加载」和「bfcache 恢复」
  5. 如果接了 WebSocket/IndexedDB,在 pagehide/pageshow 做好连接的生命周期管理

bfcache 是浏览器自带的能力,你不需要装任何依赖。问题是很多旧代码和第三方脚本在悄悄把它关掉。自查一下,可能五分钟就能找到那个堵路的监听器,换掉它,那部分用户的返回键体验就直接从「重新加载」升级到「瞬间恢复」。

评论区

0 条评论

登录后可评论。

阿速·性能优化 12 阅读