资讯文章 资讯 · NEWS
页面返回时为什么会闪一下:bfcache 的准入条件,和目录站常踩的那几条
2026-10-24 约 1,784 字 预计阅读 4 分钟 yiyunkj
页
bfcache(往返缓存)不是 HTTP 缓存,它是浏览器在用户离开时把整个页面连 JS 堆一起留在内存里的一份快照。用户点后退,页面直接解冻显示,连 load 事件都不再触发,只有 pageshow,且它的 persisted 属性为 true。
所以在这个环节上,没有哪个 HTTP 缓存策略比它更快——那是绕过网络的量级。

图片来源:网络,版权归原作者所有,侵权请联系删除
先知道值得做
Chrome 官方的使用数据:桌面端每 10 次导航有 1 次是前进或后退,移动端是每 5 次。移动端比例高的原因大家都懂,物理返回键比鼠标更随手。这意味着返还浏览路径上的体验,占据了你站点不少比例的流量,而这部分通常没人测。
Chrome 从 96 起在桌面和 Android 提供完整的 bfcache,Firefox 和 Safari 更早就有。
阻断项清单
| 阻断项 | 具体表现 | 处理办法 |
|---|---|---|
| unload 监听器 | 桌面 Chrome 与 Firefox 直接判不合格 | 改用 pagehide 做收尾 |
| Cache-Control: no-store | 主文档带上就失去资格 | 换成 no-cache 或 private 组合 |
| 打开的连接 | WebSocket、WebRTC、未结束的 IndexedDB 事务 | 在 pagehide 里关闭 |
| window.opener | 仍持有引用 | 加 rel=noopener 或置空 |
| beforeunload 提示 | 有待处理的弹窗 | 只在真有未保存内容时挂上 |
其中两条特别容易被外部脚本带进来。unload 监听可能不是你写的——第三方统计的老版本 SDK 很爱用它做「最后一次发送」,结果是整个站都进不了 bfcache。所以自查必须覆盖页面上所有脚本,不只是自己的代码。
no-store 这笔账
很多人给页面加 no-store,理由是「页面里有用户信息」。但 Chromium 团队公开的数据里,这个头单独就阻断了移动端约 17% 的历史导航(桌面约 7%),是移动端最大的单一阻断原因。
真正需要「任何地方都别存」的,只有一次口令、 Token 下发这类页面。普通登录态的正确写法是 no-cache 或 private 组合:返回时的重校验会照常发生,换来的是这一页可以直接从内存恢复。
理解这一点要抓住一个事实:bfcache 的快照只存在于那个标签页的渲染进程内存里,不落盘、也不能被别的源读取。它和「写入磁盘的共享缓存」是两回事。
怎么知道到底卡在哪
本地用 DevTools 看后退时网络面板有没有新请求:没有请求就是从内存恢复,有请求就是普通加载。这只告诉你单次结果。
线上要看 performance.getEntriesByType('navigation')[0].notRestoredReasons(Chrome 125 起)。它返回一个带原因列表的对象,比如 unload-listener,跨域 iframe 的原因会被遮成 null——注意一个第三方 iframe 就可能把整个顶层页面拖下水,而这类问题是本地 DevTools 永远发现不了的那类,只有在真实流量里才看得见。建议把它作为常规指标上报。
恢复之后:状态可能过期
整页从内存回来,意味着收藏按钮的状态、购物车数量、登录态、条目是否已收录的角标,全是离开那一刻的样子。用户本地时间和你这边的服务器之间隔了几次操作,就可能出现不一致。
处理办法统一在 pageshow 里做,用 persisted 区分是不是恢复:
window.addEventListener('pageshow', (e) => {
if (!e.persisted) return; // 普通加载,业务自己的初始化已经跑过
refreshSession();
refreshBadge();
});
window.addEventListener('pagehide', () => {
socket?.close(); // 关掉连接才进得去 bfcache
});
还有一个坑:恢复时 load 事件不会再触发。把关键初始化写在 load 里的代码等于没跑,这正是「页面回来之后什么都没动」最常见的解释。
三条落地建议
第一,把 bfcache 命中率做成指标,而不是一次优化。第三方脚本升级随时会把 unload 又带回来。
第二,所有用 target 打开新窗口的地方都要带上 rel=noopener,既是为了安全,也是为了避免留下 opener 引用。
第三,单页应用的站内 router 跳转不算 bfcache 作用域,它只管浏览器级的往返。别指望它解决 SPA 内部的前进后退。