往返缓存

发布时间:2023 年 5 月 25 日;最后更新时间:2026 年 7 月 2 日

往返缓存(简称 bfcache)是一种浏览器优化技术,可实现即时后退和前进导航。它可以显著提升浏览体验,尤其是对于网络或设备速度较慢的用户。

作为 Web 开发者,了解如何针对 bfcache 优化网页至关重要,这样您的用户才能从中受益。

浏览器兼容性

所有主流浏览器都包含 bfcache,包括自版本 96 以来的 Chrome、FirefoxSafari

往返缓存基础知识

借助往返缓存 (bfcache),当用户离开网页时,我们不会立即销毁网页,而是会延迟销毁并暂停 JavaScript 执行。如果用户很快返回,我们会再次显示该网页并恢复 JS 执行。这样一来,用户几乎可以即时完成网页导航。

您是否曾多次访问某个网站并点击链接前往另一个网页,但随后发现该网页并非您想要的,于是又点击了返回按钮?在这种情况下,bfcache 可以显著提升上一个网页的加载速度:

未启用 bfcache 系统会发起新请求来加载上一个网页,并且,根据该网页针对重复访问的 优化程度,浏览器可能需要重新下载、重新解析并重新执行刚刚下载的部分(或全部)资源。
启用 bfcache 加载上一个网页几乎是瞬间完成,因为整个网页都可以从内存中恢复,而无需访问网络。

观看此视频,了解 bfcache 在实际应用中如何加快导航速度:

使用 bfcache 可在后退和前进导航期间大幅加快网页加载速度。

在视频中,使用 bfcache 的示例比不使用 bfcache 的示例快得多。

bfcache 不仅可以加快导航速度,还可以减少流量消耗,因为无需再次下载资源。

Chrome 使用情况数据显示,在桌面设备上,每 10 次导航中有 1 次是返回或前进;在移动设备上,每 5 次导航中有 1 次是返回或前进。启用 bfcache 后,浏览器每天可以为数十亿个网页省去数据传输和加载时间!

“缓存”的运作方式

bfcache 使用的“缓存”与 HTTP 缓存不同,后者在加快重复导航速度方面发挥着自己的作用。bfcache 是内存中整个网页的快照,包括 JavaScript 堆,而 HTTP 缓存仅包含之前发出的请求的响应。由于从 HTTP 缓存中满足加载网页所需的所有请求的情况非常罕见,因此使用 bfcache 恢复的重复访问始终比即使是经过最优化处理的非 bfcache 导航更快。

冻结页面以便日后可能重新启用,这在如何最好地保留正在进行的代码方面涉及一些复杂性。例如,当网页位于 bfcache 中时,如果达到超时时间,您会如何处理 setTimeout() 调用?

答案是,浏览器会暂停 bfcache 中网页的所有待处理计时器或未解决的 Promise,包括 JavaScript 任务队列中的几乎所有待处理任务,并在网页从 bfcache 恢复时恢复处理任务。

在某些情况下(例如对于超时和 Promise),这种风险相当低,但在其他情况下,这可能会导致令人困惑或意外的行为。例如,如果浏览器暂停了作为 IndexedDB 事务一部分的必需任务,则可能会影响同一来源中的其他打开的标签页,因为多个标签页可以同时访问同一 IndexedDB 数据库。因此,浏览器通常不会尝试在 IndexedDB 事务中间或在使用可能会影响其他网页的 API 时缓存网页。

如需详细了解各种 API 用法如何影响网页的 bfcache 资格,请参阅针对 bfcache 优化网页

bfcache 和 iframe

如果网页包含嵌入式 iframe,则 iframe 本身不单独符合 bfcache 的条件。例如,如果您在 iframe 内前往其他网址,之前的 iframe 内容不会进入 bfcache;如果您返回,浏览器会在 iframe 内(而非主框架内)“返回”,但 iframe 内的返回导航不会使用 bfcache。

不过,当主框架从 bfcache 恢复时,嵌入的 iframe 将恢复为网页进入 bfcache 时的状态。

如果嵌入的 iframe 使用会阻止此行为的 API,主框架也可能会被阻止使用 bfcache。可以通过在主框架上设置权限政策或使用 sandbox 属性来避免这种情况。

bfcache 和单页应用 (SPA)

由于 bfcache 适用于浏览器管理的导航,因此不适用于单页应用 (SPA) 中的“软导航”。不过,当用户返回到 SPA 时,bfcache 仍然可以发挥作用,而无需从头开始重新初始化该应用。

用于观察 bfcache 的 API

虽然 bfcache 是浏览器自动执行的优化,但开发者仍需了解何时会发生这种情况,以便针对这种情况优化网页,并相应地调整任何指标或性能衡量

用于观察 bfcache 的主要事件是 网页过渡事件 pageshowpagehide大多数浏览器都支持这些事件。

当网页进入或离开 bfcache 时,以及在其他一些情况下(例如,当后台标签页被冻结以最大限度减少 CPU 使用量时),系统也会调度新的网页生命周期事件(freezeresume)。这些事件仅受基于 Chromium 的浏览器支持。

观察网页何时从 bfcache 恢复

网页最初加载时以及每次从 bfcache 恢复网页时,pageshow 事件都会在 load 事件之后立即触发。pageshow 事件具有 persisted 属性,如果网页是从 bfcache 恢复的,此属性为 true,否则为 false。您可以使用 persisted 属性来区分常规网页加载和 bfcache 恢复。例如:

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    console.log('This page was restored from the bfcache.');
  } else {
    console.log('This page was loaded normally.');
  }
});

在支持 Page Lifecycle API 的浏览器中,当网页从 bfcache 恢复时(紧接在 pageshow 事件之前)以及当用户重新访问冻结的后台标签页时,会触发 resume 事件。如果您想在网页冻结后(包括 bfcache 中的网页)更新网页的状态,可以使用 resume 事件;但如果您想衡量网站的 bfcache 命中率,则需要使用 pageshow 事件。在某些情况下,您可能需要同时使用这两种方法。

如需详细了解 bfcache 衡量方面的最佳实践,请参阅 bfcache 如何影响数据分析和效果衡量

观察网页何时进入 bfcache

当网页卸载或浏览器尝试将其放入 bfcache 时,pagehide 事件会触发。

pagehide 事件还具有 persisted 属性。如果值为 false,则可以确信相应网页不会立即进入 bfcache。不过,persistedtrue 并不能保证网页会被缓存。这意味着浏览器打算缓存网页,但可能存在其他因素导致无法缓存。

window.addEventListener('pagehide', (event) => {
  if (event.persisted) {
    console.log('This page *might* be entering the bfcache.');
  } else {
    console.log('This page will unload normally and be discarded.');
  }
});

同样,如果 persistedtrue,则 freeze 事件会在 pagehide 事件之后立即触发,但这仅表示浏览器打算缓存网页。但出于多种原因(稍后会介绍),它可能仍需舍弃该请求。

针对 bfcache 优化网页

并非所有网页都会存储在 bfcache 中,即使网页存储在 bfcache 中,也不会无限期保留。开发者必须了解哪些因素会使网页符合(或不符合)bfcache 的条件,才能最大限度地提高缓存命中率。

以下部分概述了相关最佳实践,可尽可能提高浏览器缓存网页的可能性。

切勿使用 unload 事件

在所有浏览器中针对 bfcache 进行优化的最重要方法是绝不使用 unload 事件。Ever!

unload 事件对浏览器来说存在问题,因为它早于 bfcache,并且互联网上的许多网页都基于以下(合理的)假设运行:在 unload 事件触发后,网页不会继续存在。这带来了挑战,因为许多此类网页在构建时假设用户每次离开时都会触发 unload 事件,但事实并非如此(这种情况已经存在很长时间了)。

因此,浏览器面临着两难的境地,它们必须在可以改善用户体验但可能也会导致网页损坏的选项之间做出选择。

在桌面设备上,Chrome 和 Firefox 选择在网页添加 unload 监听器时,使网页不符合 bfcache 的条件,这种做法风险较低,但也会使很多网页不符合条件。Safari 会尝试缓存一些带有 unload 事件监听器的网页,但为了减少潜在的损坏,它不会在用户离开时运行 unload 事件,这使得该事件非常不可靠。

在移动设备上,Chrome 和 Safari 会尝试缓存具有 unload 事件监听器的网页,因为 unload 事件在移动设备上一直非常不可靠,因此出现中断的风险较低。Firefox 会将使用 unload 的网页视为不符合往返缓存的条件,但在 iOS 上除外,因为所有浏览器都使用 WebKit 渲染引擎,因此行为与 Safari 类似。

请使用 pagehide 事件,而不是 unload 事件。只要触发 unload 事件,就会触发 pagehide 事件,并且当网页放入 bfcache 时, 事件会触发。

事实上,Lighthouse 具有 no-unload-listeners 审核功能,如果网页上的任何 JavaScript(包括来自第三方库的 JavaScript)添加了 unload 事件监听器,该功能就会向开发者发出警告。

由于 unload 事件的不可靠性以及对 bfcache 的性能影响,Chrome 正在考虑弃用该unload事件

使用权限政策来防止在网页上使用取消加载处理程序

不使用 unload 事件处理脚本的网站可以使用权限政策来确保不会添加这些脚本。

Permissions-Policy: unload=()

这还可以防止第三方或扩展程序通过添加卸载处理程序来减慢网站速度,并使网站不符合 bfcache 的条件。

仅有条件地添加 beforeunload 监听器

在现代浏览器的 bfcache 中,beforeunload 事件不会导致网页不符合 bfcache 的条件,但之前会,而且该事件仍然不可靠,因此除非绝对必要,否则请避免使用它。

不过,与 unload 事件不同,beforeunload 有正当用途。例如,当您想警告用户,如果他们离开页面,未保存的更改将会丢失时,在这种情况下,建议您仅在用户有未保存的更改时添加 beforeunload 监听器,然后在未保存的更改保存后立即移除这些监听器。

错误做法
window.addEventListener('beforeunload', (event) => {
  if (pageHasUnsavedChanges()) {
    event.preventDefault();
    return event.returnValue = 'Are you sure you want to exit?';
  }
});
此代码会无条件添加 beforeunload 监听器。
正确做法
function