# 前进后退缓存（bfcache）

> 前进后退缓存如何从内存恢复整个页面、哪些监听器和响应头会让页面在各引擎失去资格，以及 notRestoredReasons 如何报告原因。

前进后退缓存（bfcache）在用户离开页面时保存整个页面的完整快照，包括 JavaScript 堆和进行中的状态，并在之后的后退或前进导航中即时恢复而不是重新加载。恢复完全跳过网络和脚本执行，比任何 Service Worker 或 HTTP 缓存命中都快，但只有避开了一小串阻碍项的页面才有资格。

## 工作原理

离开页面时浏览器把它冻结：定时器和待处理的 Promise 被暂停而不是取消，文档在有限时间内留在内存中。后退或前进回到该条目时页面原地解冻；`pageshow` 以 `persisted === true` 触发，`load` 事件不触发。若页面已被驱逐（内存压力、时间上限或某个阻碍项），该导航就是一次普通加载（[Back/forward cache](https://web.dev/articles/bfcache)，web.dev）。各引擎都有 bfcache：Chrome 自 96 起在桌面和 Android 上提供，Firefox 和 Safari 则早得多。

### 什么让页面失去资格

- **`unload` 监听器。** 桌面版 Chrome 和 Firefox 拒绝缓存注册了它的页面；Safari 和移动版 Chrome 可能缓存然后跳过触发该事件，这让处理函数在所有引擎里都不可靠。`pagehide` 在 `unload` 会触发的所有场合触发，进入缓存时也触发，所以它是替代品。
- **主文档上的 `Cache-Control: no-store`。** 尽管 bfcache 不是 HTTP 缓存，浏览器一直拒绝缓存这类页面，所以该头只应出现在内容不得以任何形式存储的页面上。
- **打开的连接。** 导航时仍打开的 `WebSocket`、`WebRTC` 连接或 IndexedDB 事务会在 Chrome 中阻止缓存；在 `pagehide` 里关闭它们。
- **其他阻碍项。** 仍然存活的 `window.opener` 引用、待处理的 `beforeunload` 提示，以及部分版本中带 body 的进行中 `fetch()` 请求。Chrome 的清单很长，由下面的 API 按导航逐次报告。

### 读取页面未被恢复的原因

`performance.getEntriesByType('navigation')[0].notRestoredReasons`（Chrome 125；BCD `api.PerformanceNavigationTiming.notRestoredReasons`）在恢复后为 `null`，否则是一个对象，其 `reasons` 数组列出阻碍项，例如 `{ reason: "unload-listener" }`，跨源 iframe 的同一棵树被遮蔽为 `null`。Firefox 和 Safari 不暴露它；下面的 DevTools 测试工具覆盖开发期的 Chrome，现场 API 覆盖生产环境。

### 支持位置

各引擎都有 bfcache：Safari 和 Firefox 已有十余年，Chrome 自 96 起在所有平台提供（[Back/forward cache](https://web.dev/articles/bfcache)，web.dev）。可观察的差异在于上面的资格规则，以及只有 Chromium 暴露的 `notRestoredReasons`。

### 与 Service Worker 和 Core Web Vitals 的关系

bfcache 恢复完全不经过 Service Worker 和 HTTP 缓存，而且在 Core Web Vitals 工具中恢复被计为一次独立的页面访问、LCP 接近零，这正是 bfcache 资格能直接改善现场指标的原因。敏感状态（已登出的会话、过期的购物车合计）必须在检查 `event.persisted` 的 `pageshow` 处理函数里刷新，因为 `load` 不会触发。

## 示例

两个示例都在页面里而不是 Service Worker 里，因为 bfcache 是文档级特性。

### 恢复时刷新过期状态

`pageshow` 在首次加载和恢复时都运行；`persisted` 区分两者。处理函数重新检查会话并重新拉取新鲜度重要的数据，而不重新加载页面。

```js
window.addEventListener('pageshow', (event) => {
  if (!event.persisted) return; // 普通加载：load 处理函数已经运行过
  refreshSession();
  refreshCartTotal();
});

window.addEventListener('pagehide', (event) => {
  if (event.persisted) {
    socket?.close(); // 页面正进入 bfcache：释放连接
  }
});
```

在 `pagehide` 中关闭 WebSocket 是页面在 Chrome 中获得资格的关键；在 `pageshow` 中重新打开则恢复用户期待的实时状态。

### 从现场上报阻碍项

在未被恢复的页面上把 `notRestoredReasons` 发给分析系统，生产环境里真正起作用的阻碍项（例如注册了 `unload` 的第三方脚本）就能被看见。

```js
function reportBfcacheBlockers() {
  const [nav] = performance.getEntriesByType('navigation');
  if (!nav || !('notRestoredReasons' in nav)) {
    return; // 不支持（Firefox、Safari）：改用开发者工具的测试
  }
  if (nav.type === 'back_forward' && nav.notRestoredReasons) {
    navigator.sendBeacon('/analytics/bfcache', JSON.stringify(nav.notRestoredReasons));
  }
}
```

按 `nav.type === 'back_forward'` 过滤：其他导航类型上该属性虽然存在，但描述的不是页面本可改变的东西。

:::observed
Chrome DevTools 的 Application > Background services > Back/forward cache 有一个 **Test back/forward cache** 按钮，它离开页面再返回，然后报告页面已恢复，或列出按 **Actionable**、**Pending Support**、**Not Actionable** 分组的阻碍项（[Test back/forward cache](https://developer.chrome.com/docs/devtools/application/back-forward-cache)，developer.chrome.com）。带有 `window.addEventListener('unload', () => {})` 的页面会把 unload 监听器列在 **Actionable** 之下；移除监听器后在同一版本上重新测试，结果就变为已恢复。
:::

## 另请参阅

- [Back/forward cache](https://web.dev/articles/bfcache)（web.dev）
- [Test back/forward cache](https://developer.chrome.com/docs/devtools/application/back-forward-cache)（developer.chrome.com）
- [PerformanceNavigationTiming: notRestoredReasons property](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming/notRestoredReasons)（developer.mozilla.org）
- [Core Web Vitals（LCP、INP 与 CLS）](/zh/reference/performance/core-web-vitals/)
- [Speculation Rules API](/zh/reference/performance/speculation-rules/)
- [PerformanceObserver 与绘制计时](/zh/reference/performance/startup-performance/)