用户点击“后退”按钮的频率几乎超过了浏览器中的任何其他控件。他们期望之前的页面能立即出现,且位置与离开时完全一致。现代浏览器通过后退/前进缓存(bfcache)满足了这一期望。浏览器不再是在你离开页面时将其销毁,而是将其冻结在内存中。当你返回时,它会恢复一个快照。浏览器跳过了 HTML 解析、重新执行 JavaScript 和重新计算布局的过程。由于页面从未真正“死亡”,结果感觉是瞬间完成的。

bfcache 究竟是如何工作的

普通的页面加载成本很高。浏览器必须获取资源、对 HTML 进行分词、构建 DOM、运行脚本、解析样式、执行布局、绘制像素并合成图层。bfcache 通过将页面以冻结状态保存在 RAM 中,绕过了几乎所有这些步骤。它不是磁盘缓存。在用户阅读下一页时,渲染后的页面(包括 JavaScript 堆、滚动位置和表单状态)都驻留在内存中。当用户点击后退时,浏览器会解冻快照并触发 pageshow 事件。页面恢复运行,无需触碰网络或从头开始重新布局。对于使用慢速设备或不稳定连接的用户来说,bfcache 恢复与重新加载之间的差异可能达到数百毫秒甚至更多。

什么会破坏它

最近,一位开发者进行了一项干净的实验,以找出究竟是什么阻碍了 bfcache。他们构建了六个简单的页面,每个页面测试一个怀疑的阻碍因素,然后离开页面并点击后退。结果非常明确。

一个没有任何异常请求头或脚本的基准页面成功恢复。带有 beforeunload 监听器的页面也顺利恢复。令人惊讶的是,使用 Cache-Control: no-store 提供服务的页面也进入了 bfcache,这与旧的指导原则相矛盾。甚至连一个看起来动态程度过高、不适合冻结的实时博客文章也成功恢复了。

有两个页面失败了。一个带有 unload 事件监听器的页面无法恢复。一个带有打开的 WebSocket 连接的页面也被拦截了。这两个失败案例指出了每天都在困扰真实生产环境网站的陷阱。

unload 事件陷阱

unload 事件长期以来一直是进行最后时刻清理的首选信号。开发者使用它来发送分析埋点、停止定时器或清除临时状态。问题在于,bfcache 的构建理念是页面可能会重新活跃。如果浏览器检测到 unload 监听器,它会认为页面期望被彻底销毁,从而拒绝将其冻结。即使绑定的函数是空的也无济于事。仅仅是监听器的存在,就足以在所有现代浏览器中否决缓存。

替代方案是 pagehide。当页面为了 bfcache 而被冻结,以及当页面被真正丢弃时,都会触发此事件。如果你需要区分这两者,当页面进入 bfcache 时,event.persisted 属性为 true。不过,对于大多数拆卸任务,pagehide 都能涵盖这两种路径。请将所有的清理逻辑从 unload 移至 pagehide。然后彻底移除所有的 unload 监听器,包括那些隐藏在第三方分析代码片段或旧版插件中的监听器。

活动连接陷阱

打开的网络或存储连接意味着你的页面仍在进行实际工作。浏览器会在导航瞬间对活动资源进行盘点。如果它发现一个打开的 WebSocket、一个活跃的 WebRTC 对等连接或一个残留的 IndexedDB 连接,它就会中止冻结过程并正常销毁页面。在字节可能仍在流动时,快照是不可信的。

你应该在 pagehide 监听器内部关闭这些资源。调用 WebSocket 的 close 方法。关闭 WebRTC 对等连接。中止或提交任何未完成的 IndexedDB 事务。如果你的应用在用户返回时需要这些通道,请在 pageshow 中重新打开它们。这种“在 pagehide 时关闭,在 pageshow 时恢复”的模式可以让页面保持 bfcache 的资格,实现即时后退导航,同时又不丢失功能。

no-store 的意外发现

多年来,传统观点认为 Cache-Control: no-store 会阻止 bfcache。Chrome 在 2025 年改变了这一行为。现在,使用 no-store 提供服务的页面也可以进入 bfcache。只有当身份验证状态或 Cookie 发生变化,导致保存的状态失效时,浏览器才会随后移除冻结的快照。如果你一直将 no-store 作为