浏览器 bfcache(往返缓存)详解与最佳实践

2022-10-10 前端

当你点击浏览器的「后退」按钮时,有些页面瞬间加载完毕,有些则重新请求所有资源并丢失了滚动位置——这就是 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:

  1. 用户点击「后退」或「前进」按钮
  2. 用户通过手势进行页面导航(如 iOS 的滑动返回)
  3. 通过 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 恢复后会有两个问题:

  1. 定时器可能在缓存中继续执行(取决于浏览器实现)
  2. 重新恢复后,定时器逻辑可能重叠

问题 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. 页面被缓存时清理资源

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 调试

  1. 打开 Chrome DevTools → Application 面板
  2. 选择左侧的 Back/Forward Cache
  3. 这里可以看到页面是否被缓存,以及未被缓存的原因

在 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 是浏览器提供的重要优化机制,可以让页面通过前进/后退导航时瞬间加载:

  1. 原理:将完整页面快照保存在内存中,恢复时零延迟
  2. 关键事件:pageshow 和 pagehide 替代 load 和 unload
  3. 注意事项:
    • 避免使用 beforeunload 和 unload 事件
    • 设置不合适的 Cache-Control 也会阻止 bfcache
    • 从 bfcache 恢复后检查数据是否过期(通过 event.persisted)
  4. 最佳实践:
    • 初始化逻辑放在 pageshow 中统一处理
    • 页面缓存前清理定时器、连接、观察器
    • 恢复后重新初始化实时数据源(WebSocket、轮询)
  5. 收益:显著提升用户体验和 Core Web Vitals 指标

扩展阅读