愚木混株 Yumu

Back Forward Cache, 你?

是什么

使用前进/后退缓存(BFCache),我们不会在用户离开时销毁页面,而是推迟销毁页面并暂停 JS 执行。如果用户很快返回,我们会重新显示该页面并取消暂停 JS 执行。这能让用户几乎立即完成页面导航。

你有多少次访问过一个网站,点击链接想要跳转到另一个页面,却发现它不是你想要的,于是点击了返回按钮?在这种时候,bfcache 可以显著提高上一个页面的加载速度:

https://web.dev/articles/bfcache

Back Forward Cache(BFCache)是一种由现代浏览器实现的会话级缓存机制。它会在用户离开页面时,将页面完整地保存(包括 DOM 树、JavaScript 内存状态、滚动位置等),并在用户点击“返回”或“前进”按钮时,无需重新加载页面资源即可瞬间恢复页面状态

与传统的页面刷新不同,BFCache 不会触发 load/unload 事件,而是引入了新的事件机制。

生命周期事件

  • pagehide:页面被隐藏(导航离开、进入 BFCache)时触发,event.persisted 标志是否进入了 BFCache。
  • pageshow:页面被显示(包括首次加载与从 BFCache 恢复)时触发,同样可通过 event.persisted 判断。
1
2
3
4
5
6
7
window.addEventListener('pagehide', (e) => {
console.log('页面隐藏', e.persisted);
});

window.addEventListener('pageshow', (e) => {
console.log('页面显示', e.persisted);
});

判断 BFCache

要判断页面是否是从 BFCache 中恢复,可在控制台观察事件回调的 event.persisted 字段

1
2
3
4
5
window.addEventListener('pageshow', e => {
if (e.persisted) {
console.log('页面是从 BFCache 中恢复的');
}
});

启用条件与限制

虽然所有现代浏览器都支持 BFCache,但并不是所有页面都能被缓存。以下是常见的阻止因素:

条件是否阻止缓存说明
使用 unload 事件推荐使用 pagehide 替代
页面使用了 WebSocket必须手动关闭连接
beforeunload 处理逻辑会显式阻止
页面中含有 document.write不兼容
有跨域 iframe有可能会影响主页面的缓存资格

Bug 分析

在某些调试场景中会遇到这样的 bug:window 上的 click 监听器失效

页面首次加载时绑定在 window 上的 click 事件正常工作,但当用户使用浏览器“返回”功能进入页面后,该事件不再触发,尽管页面看起来和离开前一模一样。

这种现象通常发生在使用 BFCache 恢复页面时,并且是由于绑定时机或对象选择不当导致。不同的浏览器内核对于 BFCache 的处理也可能导致预期行为不一致。

复现方式

浏览器: Safari
版本:18.5 (20621.2.5.11.8)

window 上添加 click 事件监听器,这将捕获冒泡上来的所有 click 事件。正常情况下,触发点击时将正常在控制台打印出

  • consol.log(e)
  • console.log(element)
1
2
3
4
5
6
7
8
9
window.addEventListener('click', (e) => {
consol.log(e); // 正常打印
try {
var element = document.getElementById('my-element');
console.log(element); // 正常打印
} catch (error) {
console.log(error);
}
});

恢复页面后,未能正常打印两个 log

1
2
3
4
5
6
7
8
9
window.addEventListener('click', (e) => {
consol.log(e); // 正常打印
try {
var element = document.getElementById('my-element');
console.log(element); // 未被打印
} catch (error) {
console.log(error);
}
});
  • ❌ 但从 try 中 var element = document.getElementById('my-element') 开始,整个逻辑就“失联”了,连 console.log(element) 都没走到;
  • ❌ 没有错误输出,说明 catch 也没有触发。

原因分析

  1. 事件系统在恢复后可能未重新初始化到 window
    一些浏览器实现中,恢复自 BFCache 的页面并不会再次触发 window.onload,同时部分事件绑定可能因被优化而丢失。

  2. DOM 和 JS 堆内存状态虽保留,但监听器可能已被清理或被冻结
    尤其是与安全机制(如跨文档导航隔离)有关时,window 上的监听器可能未能生效。

  3. SPA 框架的行为干扰
    某些框架如 React/Vue/Next.js 会在页面恢复时重新挂载或 diff 元素,导致绑定在 window 的事件错失绑定时机。

本例原因:页面已恢复但全局事件系统未恢复

在部分浏览器中,BFCache 恢复后,window 上的事件监听器失效,但不会自动报错或移除。也就是说:
• window.addEventListener(‘click’, …) 仍在内存中;
• 但恢复后,这个监听器不再被触发,仿佛丢失绑定一样。

解决方案:经过测试

推荐解决方案

为了确保事件在 BFCache 场景下也能可靠触发,建议采取以下策略:

尽量绑定到具体元素而不是 window

1
2
3
document.addEventListener('click', handler);
// 或
document.body.addEventListener('click', handler);

pageshow 中重新绑定关键事件

1
2
3
4
5
6
7
8
9
10
11
12
function bindClick() {
document.body.addEventListener('click', handler);
}

window.addEventListener('load', bindClick);

window.addEventListener('pageshow', (e) => {
if (e.persisted) {
// 从 BFCache 恢复时重新绑定
bindClick();
}
});

避免使用 unload,改用 pagehide

1
2
3
4
5
window.addEventListener('pagehide', (e) => {
if (!e.persisted) {
// 真正离开页面才执行清理逻辑
}
});

结语

随着浏览器对性能的不断优化,BFCache 正逐步成为影响页面状态管理和事件行为的重要因素。理解它的工作原理和限制条件,是构建现代 Web 应用所必须具备的技能。

尤其在调试“事件监听器失效”这类表面难以复现的问题时,考虑是否与 BFCache 有关,是排查方向的关键突破点之一