往返缓存

往返缓存(简称 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 的网页视为不符合 bfcache 的条件,但在 iOS 上除外,因为 iOS 要求所有浏览器都使用 WebKit 渲染引擎,因此其行为与 Safari 类似。

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

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

由于 unload 事件不可靠,并且会对 bfcache 产生性能影响,Chrome 正在考虑弃用该事件

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

不使用 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 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 标头,用于指示浏览器不要将响应存储在任何 HTTP 缓存中。它适用于包含敏感用户信息(例如登录后才能访问的网页)的资源。

虽然 bfcache 不是 HTTP 缓存,但从历史上看,当在网页资源本身(而非任何子资源)上设置 Cache-Control: no-store 时,浏览器会选择不将网页存储在 bfcache 中,因此任何使用 Cache-Control: no-store 的网页可能都不符合 bfcache 的条件。我们正在努力以保护隐私的方式更改 Chrome 的这一行为

由于 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,如果未找到该 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() 控制该窗口或直接引用该窗口对象,则打开的窗口和 opener 都将不符合 bfcache 的条件。

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

如前所述,当网页保存在 bfcache 中时,系统会暂停所有已调度的 JavaScript 任务,并在网页从缓存中取出时恢复这些任务。

如果这些预定的 JavaScript 任务仅访问 DOM API 或仅限于当前页面的其他 API,那么在用户看不到页面时暂停这些任务不会造成任何问题。

不过,如果这些任务连接到同一来源中其他网页也可访问的 API(例如 IndexedDB、Web Locks、WebSockets),则可能会出现问题,因为暂停这些任务可能会阻止其他标签页中的代码运行。

因此,在以下情况下,某些浏览器不会尝试将网页放入 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.onupgradeneeded = () => req.result.createObjectStore('keyval');
      req.onerror = () => reject(req.error);
      req.onsuccess = () => 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. 在开发者工具中,依次前往应用 -> 往返缓存
  3. 点击运行测试按钮。然后,开发者工具会尝试离开网页再返回,从而确定该网页能否从 bfcache 恢复。
开发者工具中的往返缓存面板
开发者工具中的往返缓存面板。

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

DevTools 报告网页已成功从 bfcache 恢复
已成功恢复的网页。

如果不成功,面板会说明原因。如果原因是开发者可以解决的问题,面板会将其标记为可采取行动

DevTools 报告未能从 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 恢复视为数据集中的单独网页访问。虽然没有专门用于在 bfcache 恢复后衡量这些指标的 Web 性能 API,但您可以使用现有的 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 如何影响各个指标,请参阅各个 Core Web Vitals 指标指南页面。如需查看如何实现这些指标的 bfcache 版本的具体示例,请参阅将这些指标添加到 web-vitals JS 库的 PR

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

其他资源