往返缓存

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

本页概述了如何在所有浏览器中针对 bfcache 优化网页

浏览器兼容性

我们多年来在桌面设备和移动设备上同时在 FirefoxSafari 上支持 bfcache。

从版本 86 开始,Chrome 为一小部分用户启用了 bfcache 在 Android 上实现跨网站导航。在后续版本中,会逐步推出更多支持。从版本 96 开始,系统会为桌面设备和移动设备上的所有 Chrome 用户启用 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 和单页应用 (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 并不能保证网页会被缓存。这意味着浏览器打算缓存网页,但可能有其他因素导致无法缓存。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 资格条件,并推荐了一些最佳实践,以便最大限度地提高浏览器缓存网页的能力,从而降低缓存命中率。

使用 pagehide,而不要使用 unload

在所有浏览器中针对 bfcache 进行优化的最重要方法是一律不使用 unload 事件监听器。请监听 pagehide,因为它在网页进入 bfcache 时和在 unload 每次触发时都会触发。

unload 是一项较旧的功能,最初设计为在用户离开某个页面时触发。情况不再如此,但许多网页仍会假设浏览器以这种方式使用 unload,并且在 unload 触发后,未加载的网页将停止存在。如果浏览器尝试缓存未加载的网页,这可能会破坏 bfcache。

在桌面设备上,Chrome 和 Firefox 会使具有 unload 监听器的页面不符合 bfcache 的条件,这可以降低风险,但也会导致大量页面无法缓存,进而使重新加载速度变慢。Safari 确实会尝试通过 unload 事件监听器缓存某些页面,但为了减少潜在的中断,它不会在用户离开网页时运行 unload 事件,这会导致 unload 监听器变得不可靠。

在移动设备上,Chrome 和 Safari 会尝试使用 unload 事件监听器来缓存页面,因为 unload 在移动设备上的不可靠性会降低网站服务中断的风险。Firefox 移动版会将使用 unload 的页面视为不符合 bfcache 的条件,但 iOS 除外,该平台要求所有浏览器使用 WebKit 渲染引擎,因此其行为类似于 Safari。

如需确定您网页上的 JavaScript 是否使用了 unload,我们建议您使用 Lighthouse 中的 no-unload-listeners 审核

如需了解 Chrome 废弃 unload 的计划,请参阅废弃 unload 事件

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

某些第三方脚本和扩展程序可以向网页添加卸载处理程序,这会导致网站不符合 bfcache 资格条件,从而降低网站速度。为防止在 Chrome 115 及更高版本中出现这类问题,请使用权限政策

Permission-Policy: unload()

仅有条件地添加 beforeunload 监听器

beforeunload 事件不会导致您的网页不符合 bfcache 资格条件。但它不可靠,因此我们建议仅在绝对必要的情况下使用。

beforeunload 的一个示例用例是警告用户,如果他们离开页面,将丢失未保存的更改。在这种情况下,我们建议仅在用户有未保存的更改时添加 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);
});

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

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

虽然 bfcache 不是 HTTP 缓存,但对于页面资源(而不是任何子资源)设置了 Cache-Control: no-store,浏览器历来已从 bfcache 中排除页面。Chrome 正努力更改此行为,同时保护用户隐私,但默认情况下,使用 Cache-Control: no-store 的网页不符合 bfcache 的条件。

如需针对 bfcache 进行优化,请仅对包含不得缓存的敏感信息的页面使用 Cache-Control: no-store

如果页面希望始终提供最新内容但不包含敏感信息,请使用 Cache-Control: no-cacheCache-Control: max-age=0。这些操作会告知浏览器在提供内容之前重新验证内容,并且不会影响网页的 bfcache 资格,因为从 bfcache 恢复页面不涉及 HTTP 缓存。

如果您的内容每分钟都会更改,请改用 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 Publishing 代码示例所示。如需详细了解适用于您的网站的最佳做法,请与您的广告提供商联系。

避免 window.opener 引用

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

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

为避免这些风险,请使用 rel="noopener" 来阻止创建 window.opener 引用。这是所有现代浏览器中的默认行为。如果您的网站需要打开一个窗口并使用 window.postMessage() 或通过直接引用 window 对象对其进行控制,则打开的窗口和打开器都不符合 bfcache 的条件。

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

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

如果这些预定的 JavaScript 任务仅访问 DOM API 或与当前页面隔离的其他 API,那么在页面对用户不可见时暂停这些任务不会导致问题。

但是,如果这些任务连接到的 API 也可从同一来源的其他页面(例如 IndexedDB、Web Lock 和 WebSocket)访问,则暂停这些任务可能会阻止这些页面上的代码运行,从而破坏这些页面。

因此,如果网页具有以下某个条件,某些浏览器不会尝试将该网页放入 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. 点击 Run Test 按钮。然后,开发者工具会尝试离开和返回页面,以确定是否可以从 bfcache 恢复该页面。
开发者工具中的往返缓存面板
开发者工具中的往返缓存面板。

如果测试成功,该面板会报告“已从往返缓存中恢复”。 如果上传失败,面板会说明原因。如需查看原因的完整列表,请参阅 Chromium 的“无法恢复的原因”列表

如果原因属于开发者可以解决的问题,面板会将其标记为可操作

开发者工具报告从 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 的网页,请按如下方式衡量网页加载的导航类型:

// 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 导航都达到 100% bfcache 命中率。但是,衡量其比率有助于识别阻止使用 bfcache 的网页。

Chrome 团队正致力于开发 NotRestoredReasons API,以帮助揭示网页不使用 bfcache 的原因,以便开发者提高其 bfcache 的命中率。

性能衡量

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

由于 bfcache 导航会恢复现有页面,而不是开始新的页面加载,因此在 bfcache 启用后,收集的页面加载总数会减少。但是,bfcache 替换的网页可能是数据集内加载速度最快的网页之一,因为重复网页加载(包括往返导航),并且受 HTTP 缓存的影响,通常比首次加载速度更快。因此,启用 bfcache 仍会导致 Google Analytics(分析)显示网页加载速度变慢,尽管可为用户提升网站性能。

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

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

对核心网页指标的影响

核心网页指标能够从多个维度(加载速度、互动性和视觉稳定性)衡量用户的网页体验。 核心网页指标必须反映这样一个事实,即用户体验 bfcache 在恢复后导航的速度要快于默认网页加载速度。

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

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

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

其他资源