往返缓存

往返缓存(即 bfcache)是一种浏览器优化方法,可实现即时的往返导航。这可显著提升浏览体验,尤其是对于网络或设备速度较慢的用户而言。

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

浏览器兼容性

多年来,我们在 FirefoxSafari 上一直支持 bfcache,在桌面设备和移动设备上都可以使用。

从版本 86 开始,Chrome 为一小部分用户在 Android 设备上启用了 bfcache 以便在 Android 设备上进行跨网站导航。在后续版本中,我们会逐步推出更多支持。从版本 96 开始,系统会为桌面设备和移动设备上的所有 Chrome 用户启用 bfcache。

bfcache 基础知识

bfcache 是内存中的缓存,可在用户离开网页时存储网页(包括 JavaScript 堆)的完整快照。将整个网页保存在内存中后,当用户决定返回时,浏览器就可以快速恢复该网页。

您访问某个网站并点击链接转到另一个网页有多少次,但意识到这不是您想要的,然后点击了返回按钮?此时,bfcache 在很大程度上可以提高前一个网页的加载速度:

未启用 bfcache 系统会发起新的请求以加载上一页,并且根据该网页针对重复访问的 优化程度,浏览器可能需要重新下载、重新解析和重新执行刚刚下载的部分(或所有)资源。
启用 bfcache 上一页的加载操作实质上是即时的,因为整个网页可以从内存中恢复,而无需连接至网络。

请观看这段视频,了解 bfcache 能如何提升导航速度:

使用 bfcache 在往返导航期间可以更快速地加载网页。

在视频中,带有 bfcache 的示例比没有它的示例快得多。

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

Chrome 使用情况数据显示,桌面设备上的导航中有 1/10 的导航,移动设备上有 1/5 的导航是后退或前进。启用 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 内“返回”(而不是在主框架中),但 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

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

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

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 事件之后立即触发,但这仅表示浏览器打算缓存网页。intends不过,出于下文介绍的多种原因,它可能仍必须舍弃它们。

针对 bfcache 优化网页

并非所有网页都会存储在 bfcache 中,即使某个页面存储在 bfcache 中,它也不会无限期地保留在此处。开发者务必要了解网页为何符合(以及不符合)bfcache 资格条件,以便最大限度地提高缓存命中率。

下文概述了一些最佳实践,以便让浏览器尽可能缓存您的网页。

切勿使用 unload 事件

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

unload 事件会给浏览器带来问题,因为它早于 bfcache,并且互联网上的许多网页都是在 unload 事件触发后网页不会继续存在的(合理)假设下运行的。这带来一定的挑战,因为其中许多网页在构建时假设每次用户离开网页时都会触发 unload 事件,但这种情况已不再是正确的(并且很长一段时间内并非如此)。

因此,浏览器面临着两大困境,那就是它们必须在两者之间做出选择,既能改善用户体验,又有破坏网页的风险。

在桌面设备上,Chrome 和 Firefox 已选择将网页设为不符合 bfcache 的条件,但前提是要添加 unload 监听器,这样风险较低,但也不符合很多网页的条件。Safari 会尝试使用 unload 事件监听器缓存某些页面,但为了减少潜在的中断,它不会在用户离开网页时运行 unload 事件,这会导致事件非常不可靠。

在移动设备上,Chrome 和 Safari 会尝试使用 unload 事件监听器缓存网页,因为 unload 事件在移动设备上一直非常不可靠,所以破坏的风险较低。Firefox 会将使用 unload 的网页视为不符合 bfcache 的条件,但在 iOS 中除外。由于该系统要求所有浏览器使用 WebKit 呈现引擎,因此其行为方式与 Safari 类似。

请改用 pagehide 事件,而不是 unload 事件。在触发 unload 事件的所有情况下,都会触发 pagehide 事件;当网页被放入 bfcache 时,它也会触发。

事实上,Lighthouse 提供 no-unload-listeners 审核,如果其网页上的 JavaScript(包括来自第三方库的 JavaScript)添加了 unload 事件监听器,系统会向开发者发出警告。

鉴于 bfcache 不可靠以及会对性能产生负面影响,因此 Chrome 希望弃用 unload 事件

使用权限政策防止在网页上使用卸载处理程序

未使用 unload 事件处理脚本的网站可以使用 Chrome 115 中的权限政策来确保不会添加此类事件。

Permission-Policy: unload()

此外,这样还可以防止第三方或扩展程序通过添加卸载处理程序并使网站不符合 bfcache 的条件,从而降低网站的加载速度。

仅有条件地添加 beforeunload 监听器

beforeunload 事件不会使您的网页不符合现代浏览器 bfcache 中的 bfcache 条件,但以前是这样做的,并且它仍然不可靠,因此除非绝对必要,否则请避免使用它。

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

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

// A function that invokes a callback when the page has unsaved changes.
onPageHasUnsavedChanges(() => {
  window.addEventListener('beforeunload', beforeUnloadListener);
});

// A function that invokes a callback when the page's unsaved changes are resolved.
onAllChangesSaved(() => {
  window.removeEventListener('beforeunload', beforeUnloadListener);
});
此代码仅在需要时添加 beforeunload 监听器(在不需要时将其移除)。

尽可能减少使用 Cache-Control: no-store

Cache-Control: no-store 是一个 HTTP 标头 Web 服务器,它可以针对响应设置,指示浏览器不要将响应存储在任何 HTTP 缓存中。它用于包含敏感用户信息的资源,例如需要登录才能访问的网页。

虽然 bfcache 不是 HTTP 缓存,但过去,当在页面资源本身(而不是在任何子资源)上设置 Cache-Control: no-store 时,浏览器会选择不将该页面存储在 bfcache 中。我们正在努力以可保护隐私的方式针对 Chrome 更改此行为,但目前所有使用 Cache-Control: no-store 的网页都不符合 bfcache 的条件。

由于 Cache-Control: no-store 会限制网页的 bfcache 资格,因此应仅在不适合进行任何类型的缓存中包含敏感信息的网页上设置。

对于需要始终提供最新内容(且内容不包含敏感信息)的网页,请使用 Cache-Control: no-cacheCache-Control: max-age=0。这些指令指示浏览器在提供内容之前重新验证内容,并且不会影响网页的 bfcache 资格。

请注意,从 bfcache 恢复某个网页时,它是从内存(而不是 HTTP 缓存)中恢复的。因此,系统不会考虑 Cache-Control: no-cacheCache-Control: max-age=0 等指令,并且在向用户显示内容之前不会执行重新验证。

不过,这仍然有可能带来更好的用户体验,因为 bfcache 能即时恢复,而且网页不会在 bfcache 中很长时间,因此内容不太可能过期。不过,如果您的内容每分钟都发生更改,您可以使用 pageshow 事件获取任何更新,如下一部分所述。

在 bfcache 恢复后更新过时或敏感数据

如果您的网站保留了用户状态(尤其是任何敏感的用户信息),那么当网页从 bfcache 恢复后,您就需要更新或清除这些数据。

例如,如果用户导航到结账页,然后更新了购物车,那么当从 bfcache 恢复过时页面时,返回导航可能会暴露过时的信息。

另一个更严重的例子是,某位用户在公共计算机上退出网站,然后下一位用户点击了返回按钮。这可能会泄露用户以为在退出时已被清除的私密数据。

为避免这种情况,如果 event.persistedtrue,最好始终在 pageshow 事件后更新网页:

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // Do any checks and updates to the page
  }
});

虽然理想情况下,您需要就地更新内容,但对于某些更改,您可能需要强制完全重新加载。以下代码会检查 pageshow 事件中是否存在网站特定的 Cookie,如果未找到,则会重新加载:

window.addEventListener('pageshow', (event) => {
  if (event.persisted && !document.cookie.match(/my-cookie)) {
    // Force a reload if the user has logged out.
    location.reload();
  }
});

重新加载的优势在于仍会保留历史记录(以便向前导航),但在某些情况下,重定向可能更合适。

广告和 bfcache 恢复

您可能很想避免使用 bfcache 在每次往返导航时投放一组新的广告。不过,除了对广告效果产生影响之外,此类行为是否能够提升广告互动度也存在疑问。用户可能注意到了他们原本打算返回点击的广告,但这些广告是重新加载的,而不是从 bfcache 中恢复的。在做出假设之前,请务必对此场景进行测试(最好通过 A/B 测试)。

如果网站希望在 bfcache 恢复时刷新广告,因此当 event.persistedtrue 时仅刷新 pageshow 事件中的广告,可以在不影响网页性能的情况下实现这一点。请与您的广告提供商联系,但可以参考这个示例,了解如何使用 Google 发布商代码执行此操作

避免 window.opener 引用

在旧版浏览器中,如果页面是使用 window.open() 通过包含 target=_blank 的链接打开的,而不指定 rel="noopener",则打开的页面会引用所打开页面的窗口对象。

除了存在安全风险之外,包含非 null window.opener 引用的网页无法安全地放入 bfcache 中,因为这可能会破坏任何尝试访问它的网页。

因此,最好避免创建 window.opener 引用。您可以尽可能使用 rel="noopener" 执行此操作(请注意,目前这在所有现代浏览器中是默认设置)。如果您的网站需要打开一个窗口,并通过 window.postMessage() 或直接引用 window 对象对其进行控制,则打开的窗口和 opener 都无法储存至 bfcache。

在用户离开之前关闭打开的连接

如前所述,将网页放入 bfcache 时,它会暂停所有计划的 JavaScript 任务,并在页面从缓存中取出后恢复这些任务。

如果这些预定的 JavaScript 任务仅访问 DOM API(或仅与当前网页隔离的其他 API),那么在用户看不到网页时暂停这些任务不会引发任何问题。

但是,如果这些任务关联到也可从同一源的其他页面(例如 IndexedDB、Web Lock、WebSocket)访问的 API,则可能会出现问题,因为暂停这些任务可能会阻止其他标签页中的代码运行。

因此,在以下情况下,某些浏览器不会尝试将网页放入 bfcache:

如果您的网页使用了其中任何 API,我们强烈建议您在 pagehidefreeze 事件期间关闭连接,并移除或断开连接观察者。这样一来,浏览器就可以安全地缓存网页,而不会影响其他打开的标签页。

接下来,如果该网页从 bfcache 恢复,您可以在 pageshowresume 事件期间重新打开或重新连接到这些 API。

以下示例展示了如何通过在 pagehide 事件监听器中关闭打开的连接来确保使用 IndexedDB 的网页符合 bfcache 的条件:

let dbPromise;
function openDB() {
  if (!dbPromise) {
    dbPromise = new Promise((resolve, reject) => {
      const req = indexedDB.open('my-db', 1);
      req. => req.result.createObjectStore('keyval');
      req. => reject(req.error);
      req. => resolve(req.result);
    });
  }
  return dbPromise;
}

// Close the connection to the database when the user leaves.
window.addEventListener('pagehide', () => {
  if (dbPromise) {
    dbPromise.then(db => db.close());
    dbPromise = null;
  }
});

// Open the connection when the page is loaded or restored from bfcache.
window.addEventListener('pageshow', () => openDB());

测试以确保您的网页可缓存

借助 Chrome 开发者工具,您可以测试自己的网页,确保它们已针对 bfcache 进行了优化,并找出任何可能阻止网页获取符合条件的问题。

若要测试网页,请按以下步骤操作:

  1. 在 Chrome 中前往相应网页。
  2. 在开发者工具中,前往 Application -> Back-forward Cache
  3. 点击 Run Test 按钮。然后,开发者工具会尝试离开和返回页面,以确定是否可以从 bfcache 恢复该页面。
开发者工具中的往返缓存面板
开发者工具中的往返缓存面板。

如果测试成功,该面板会报告“已从往返缓存中恢复”。

开发者工具报告页面已成功从 bfcache 恢复
已成功恢复的页面。

如果上传失败,面板会说明原因。如果原因属于开发者可以解决的问题,面板会将其标记为可操作

开发者工具报告从 bfcache 恢复页面失败
bfcache 测试失败,提供可操作结果。

在此示例中,使用unload事件监听器会使网页不符合处理 bfcache 的条件。您可以通过从 unload 改用 pagehide 来解决此问题:

正确做法
window.addEventListener('pagehide', ...);
错误做法
window.addEventListener('unload', ...);

Lighthouse 10.0 还添加了 bfcache 审核,用于执行类似的测试。如需了解详情,请参阅 bfcache 审核的文档

bfcache 如何影响分析和性能衡量

如果您使用分析工具来衡量网站的访问情况,您可能会发现,由于 Chrome 为更多用户启用了 bfcache,因此报告的网页浏览总量有所减少。

事实上,您可能已经漏掉了其他实现 bfcache 的浏览器带来的网页浏览量,因为许多热门分析库不会将 bfcache 恢复为新的网页浏览量,从而衡量结果。

若要在网页浏览量中包含 bfcache 恢复,请设置 pageshow 事件的监听器并检查 persisted 属性。

以下示例展示了如何使用 Google Analytics(分析)执行此操作。其他分析工具可能使用类似的逻辑:

// Send a pageview when the page is first loaded.
gtag('event', 'page_view');

window.addEventListener('pageshow', (event) => {
  // Send another pageview if the page is restored from bfcache.
  if (event.persisted) {
    gtag('event', 'page_view');
  }
});

衡量 bfcache 命中率

您还可以衡量是否使用了 bfcache,以帮助识别未使用 bfcache 的网页。这可以通过衡量网页加载的导航类型来实现:

// Send a navigation_type when the page is first loaded.
gtag('event', 'page_view', {
   'navigation_type': performance.getEntriesByType('navigation')[0].type;
});

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // Send another pageview if the page is restored from bfcache.
    gtag('event', 'page_view', {
      'navigation_type': 'back_forward_cache';
    });
  }
});

使用 back_forward 导航和 back_forward_cache 导航的计数来计算 bfcache 命中率。

请务必注意,在网站所有者控制范围之外,往返导航不会使用 bfcache 时,有很多情况,包括:

  • 当用户退出浏览器并再次启动时触发
  • 当用户复制标签页时触发
  • 当用户关闭标签页并重新打开时触发

在某些情况下,某些浏览器可能会保留原始导航类型,因此尽管不是返回/前进导航,但仍可能会显示某种 back_forward 类型。

即使没有这些排除项,bfcache 也会在一段时间后被丢弃以节省内存。

因此,网站所有者不应期望所有 back_forward 导航的 bfcache 命中率都是 100%。不过,衡量它们的比率有助于找出网页本身在很大比例的往返导航中阻止 bfcache 使用的网页。

Chrome 团队添加了 NotRestoredReasons API 来帮助揭示网页未使用 bfcache 的原因,以便开发者提高 bfcache 的命中率。Chrome 团队还在 CrUX 中添加了导航类型,因此即使不自行测量,也可以查看 bfcache 导航次数。

性能衡量

bfcache 可能会对实测中收集的性能指标产生负面影响,尤其是衡量网页加载时间的指标。

由于 bfcache 导航会恢复现有网页,而不是启动新网页加载,因此启用 bfcache 后,收集的网页加载总数会减少。但重要的是,页面加载操作可能被 bfcache 恢复所取代,它们可能是数据集中速度最快的页面加载次数之一。这是因为,按照定义,往返导航属于重复访问,重复网页加载速度通常比首次访问者加载网页更快(由于采用 HTTP 缓存,如前所述)。

结果是数据集中的网页加载速度较快,而这可能会导致分布速度变慢,尽管用户体验的性能可能会有所提升!

有几种方法可以解决此问题。一种是为所有网页加载指标添加注解,分别使用它们各自的导航类型:navigatereloadback_forwardprerender这样,即使总体分布情况出现偏向不利的情况,您也可以继续监控这些导航类型中的广告效果。对于不以用户为中心的网页加载指标,例如首字节时间 (TTFB),我们建议使用此方法。

对于核心网页指标等以用户为中心的指标,更好的选择是报告能更准确地反映用户体验的值。

对核心网页指标的影响

核心网页指标能够针对各种维度(加载速度、互动性和视觉稳定性)衡量用户的网页体验。由于用户体验 bfcache 恢复时的导航速度要快于整个网页加载速度,因此核心网页指标必须反映这一点。毕竟,用户并不在意是否启用了 bfcache,他们只关心导航速度很快!

收集核心网页指标并生成相关报告的工具(如 Chrome 用户体验报告)会在其数据集内将 bfcache 恢复视为单独的网页访问。虽然没有专用的 Web 性能 API 在 bfcache 恢复后衡量这些指标,但您可以使用现有的 Web API 近似值:

  • 对于 Largest Contentful Paint (LCP),请使用 pageshow 事件的时间戳与下一个绘制帧的时间戳之间的增量,因为系统将同时绘制帧中的所有元素。如果是 bfcache 恢复,LCP 和 FCP 是相同的。
  • 对于 Interaction to Next Paint (INP),请继续使用现有的 Performance Observer,但将当前的 INP 值重置为 0。
  • 对于 Cumulative Layout Shift (CLS),请继续使用现有的 Performance Observer,但将当前 CLS 值重置为 0。

如需详细了解 bfcache 对各个指标的影响,请参阅各个核心网页指标指标指南页面。有关如何实现这些指标的 bfcache 版本的具体示例,请参阅将这些指标添加到 web-vitals JS 库中的 PR

web-vitals JavaScript 库在其报告的指标中支持 bfcache 恢复

其他资源