当你点击浏览器的「后退」按钮时,有些页面瞬间加载完毕,有些则重新请求所有资源并丢失了滚动位置——这就是 bfcache(Back-Forward Cache,往返缓存) 在起作用。本文将深入 bfcache 的原理、生命周期、相关事件以及最佳实践。
什么是 bfcache?
bfcache(Back-Forward Cache)是浏览器的一种优化机制。当用户导航离开一个页面时,浏览器会将整个页面的完整快照(包括 JavaScript 堆、DOM 状态、滚动位置)保存在内存中。当用户通过「前进」或「后退」按钮返回时,浏览器直接从内存中恢复页面,而不是重新加载。
传统方式:点击后退 → 重新发送 HTTP 请求 → 下载 HTML/CSS/JS → 重新渲染 → 展示页面
└── 整个过程可能耗时数百毫秒到数秒
bfcache: 点击后退 → 从内存中恢复页面快照 → 瞬间展示
└── 几乎零延迟bfcache 的关键特性
- 完全恢复:DOM、JavaScript 状态、滚动位置、表单输入全部恢复
- 零网络请求:命中 bfcache 时不会发送任何 HTTP 请求
- 零渲染开销:页面在内存中是完整的,只需恢复到屏幕上
bfcache 的生命周期
进入 bfcache 的条件
在以下情况下,页面可能被放入 bfcache:
- 用户点击「后退」或「前进」按钮
- 用户通过手势进行页面导航(如 iOS 的滑动返回)
- 通过
history.back()、history.forward()、history.go()导航
阻止 bfcache 的因素
并非所有页面都能进入 bfcache。以下因素会阻止浏览器缓存页面:
1. 注册了 beforeunload 事件监听器
这是最常见的原因!在 Chrome 85+ 中,注册了 beforeunload 监听器的页面不会被 bfcache 缓存。
// ❌ 这会阻止 bfcache
window.addEventListener('beforeunload', (event) => {
event.preventDefault();
});
// ✅ 只在需要时才注册,不需要时及时移除2. 使用 XMLHttpRequest 或 fetch 进行中的请求
当页面导航离开时,如果存在未完成的网络请求,浏览器可能不会缓存页面。
3. 存在 window.open() 打开的窗口
如果在页面中打开过其他窗口,当前页面可能不会被缓存。
4. 设置了不可缓存的 Cache-Control
Cache-Control: no-store /* 不仅 HTTP 缓存失效,也会影响 bfcache */5. 使用了 unload 事件监听器
// ❌ unload 事件会降低 bfcache 命中率
window.addEventListener('unload', () => {
// 清理操作...
});6. 使用了 WebSocket 等持久连接
存在活跃的 WebSocket 连接时,bfcache 可能不可用。
7. 页面包含中等的 iframe
所有 iframe 也需满足 bfcache 条件,否则整个页面无法缓存。
| 阻止因素 | 影响程度 | 说明 |
|---|---|---|
beforeunload 事件 |
🔴 强烈阻止(Chrome 85+) | 最常见的问题 |
unload 事件 |
🔴 强烈阻止 | 使用 pagehide 替代 |
Cache-Control: no-store |
🔴 阻止 HTTP 和 bfcache | 仅对需要保密的页面使用 |
| 活跃的 WebSocket | 🟡 可能阻止 | 连接不会在缓存中保持 |
| 未完成的网络请求 | 🟡 可能阻止 | 离开前确保请求完成 |
| iframe 缓存限制 | 🟡 iframe 也需符合条件 | 检查第三方组件 |
bfcache 相关事件
bfcache 提供了两个专用的事件:pageshow 和 pagehide。
pageshow 事件
每次页面显示时触发——无论页面是新加载还是从 bfcache 恢复。
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// 从 bfcache 中恢复
console.log('🚀 页面从 bfcache 恢复');
// 此时 DOM、滚动位置、JS 状态都已恢复
// 但可能需要重新初始化某些资源
refreshData();
} else {
// 首次加载
console.log('🆕 页面首次加载');
initializePage();
}
});event.persisted 属性的值:
true:页面是从 bfcache 恢复的false:页面是新加载的
pagehide 事件
页面被隐藏(即将被缓存或销毁)时触发。unload 事件的推荐替代者。
window.addEventListener('pagehide', (event) => {
if (event.persisted) {
// 页面将被放入 bfcache
console.log('💾 页面将被缓存到 bfcache');
} else {
// 页面将被销毁
console.log('🗑️ 页面将被销毁');
}
});事件执行顺序对比
首次加载页面:
1. DOMContentLoaded
2. load
3. pageshow (persisted = false)从 bfcache 恢复(点击后退):
1. pageshow (persisted = true) ← 瞬间触发,没有网络请求!页面被缓存(离开页面):
1. pagehide (persisted = true) ← 页面要被放入 bfcache页面被卸载(不进入 bfcache):
1. pagehide (persisted = false)
2. beforeunload ← 如果有注册
3. unload ← 如果有注册在 bfcache 中需要处理的问题
问题 1:过期的数据
// ❌ 问题:从 bfcache 恢复后,数据可能已经过期
window.addEventListener('pageshow', (event) => {
// 其他初始化代码...
// 忘记处理 bfcache 恢复的场景
});问题 2:定时器仍然在运行
如果页面被缓存时没有清除定时器,从 bfcache 恢复后会有两个问题:
- 定时器可能在缓存中继续执行(取决于浏览器实现)
- 重新恢复后,定时器逻辑可能重叠
问题 3:WebSocket 连接已断开
页面被缓存时,WebSocket 连接会被关闭。恢复后需要重新建立连接。
问题 4:动画和资源状态
CSS 动画、视频播放、音频播放等状态需要重置。
最佳实践清单
1. 用 pageshow 替代传统初始化入口
// ❌ 不推荐:只处理首次加载
window.addEventListener('load', () => {
initializeApp();
});
// ✅ 推荐:同时处理首次加载和 bfcache 恢复
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// 从 bfcache 恢复:刷新数据
console.log('从 bfcache 恢复,刷新数据');
refreshData();
} else {
// 首次加载
initializeApp();
}
});2. 用 pagehide 替代 unload
// ❌ 不推荐:unload 事件会阻止 bfcache
window.addEventListener('unload', () => {
cleanupResources();
});
// ✅ 推荐:pagehide 不会阻止 bfcache
window.addEventListener('pagehide', () => {
cleanupResources();
});3. 页面被缓存时清理资源
const timers = new Set();
const observers = new Set();
function startTimer(callback, delay) {
const id = setInterval(callback, delay);
timers.add(id);
return id;
}
function createObserver(target, config, callback) {
const observer = new IntersectionObserver(callback, config);
observer.observe(target);
observers.add(observer);
return observer;
}
// 页面被缓存时清理
window.addEventListener('pagehide', () => {
if (event.persisted) {
// 清理所有定时器
timers.forEach(id => clearInterval(id));
timers.clear();
// 清理所有观察器
observers.forEach(obs => obs.disconnect());
observers.clear();
// 关闭 WebSocket 连接
if (window.socket) {
window.socket.close();
}
}
});
// 从 bfcache 恢复时重新初始化
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
console.log('从 bfcache 恢复,重置资源');
// 重新建立 WebSocket 连接
connectWebSocket();
// 重新注册定时器
startPolling();
}
});4. 不要使用 beforeunload 做分析统计
// ❌ 错误用法:发送分析数据
window.addEventListener('beforeunload', () => {
navigator.sendBeacon('/api/analytics', data);
});
// ✅ 正确用法:使用 pagehide 或 Visibility API
window.addEventListener('pagehide', () => {
navigator.sendBeacon('/api/analytics', data);
});
// 或者使用 Visibility API
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
navigator.sendBeacon('/api/analytics', data);
}
});5. 图片和懒加载处理
// 图片懒加载在 bfcache 恢复后可能需要重新触发
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// 重新触发图片懒加载
const lazyImages = document.querySelectorAll('img[data-src]');
lazyImages.forEach(img => {
if (isElementInViewport(img)) {
img.src = img.dataset.src;
}
});
}
});调试 bfcache
Chrome DevTools 调试
- 打开 Chrome DevTools → Application 面板
- 选择左侧的 Back/Forward Cache
- 这里可以看到页面是否被缓存,以及未被缓存的原因
在 Console 中检查
// 在生产环境中检测 bfcache 命中情况
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
console.log('%c[bfcache] 命中!页面从缓存恢复', 'color: green; font-weight: bold');
} else {
console.log('%c[bfcache] 未命中,页面是新加载的', 'color: orange;');
}
});使用 Performance API
// 通过 Navigation Timing API 检测
const entries = performance.getEntriesByType('navigation');
entries.forEach(entry => {
if (entry.type === 'back_forward') {
console.log('⚠️ 这是通过前进/后退导航到此页面的');
console.log(` - 是否来自 bfcache: ${!triggeredByPopstate}`);
}
});性能影响
bfcache 的收益
| 指标 | 传统加载 | bfcache 恢复 | 改进 |
|---|---|---|---|
| FCP(首次内容绘制) | 500~2000ms | 几乎 0ms | 🟢 优秀 |
| LCP(最大内容绘制) | 800~3000ms | 几乎 0ms | 🟢 优秀 |
| 网络请求 | 多个 HTTP 请求 | 0 个请求 | 🟢 零请求 |
| JS 执行 | 完整执行 | 不重新执行 | 🟢 零开销 |
Core Web Vitals 影响
bfcache 对 Core Web Vitals 有显著正面影响:
- FCP 和 LCP 可能降低 30%~80%
- CLS(累积布局偏移) 在 bfcache 恢复时为零,因为布局已经稳定
常见应用场景
电商网站的商品详情页
// 用户浏览商品 → 查看详情 → 点击后退
// 有 bfcache:商品列表页瞬间恢复,滚动位置保留
// 无 bfcache:需要重新加载列表页,可能滚动到页面顶部博客或文档站点
// 用户在文章列表中点击一篇文章 → 阅读 → 点击后退
// bfcache 保留了列表页的完整状态
// 包括:滚动位置、阅读进度、搜索关键词表单页面
// 用户填写表单 → 切换到其他标签页查资料 → 后退回来
// bfcache 保留了所有已填写的表单数据
// 用户不需要重新填写!总结
bfcache 是浏览器提供的重要优化机制,可以让页面通过前进/后退导航时瞬间加载:
- 原理:将完整页面快照保存在内存中,恢复时零延迟
- 关键事件:
pageshow和pagehide替代load和unload - 注意事项:
- 避免使用
beforeunload和unload事件 - 设置不合适的
Cache-Control也会阻止 bfcache - 从 bfcache 恢复后检查数据是否过期(通过
event.persisted)
- 避免使用
- 最佳实践:
- 初始化逻辑放在
pageshow中统一处理 - 页面缓存前清理定时器、连接、观察器
- 恢复后重新初始化实时数据源(WebSocket、轮询)
- 初始化逻辑放在
- 收益:显著提升用户体验和 Core Web Vitals 指标