后退/前进缓存

后退/前进缓存(或 bfcache)是一种浏览器优化,可实现即时后退和前进导航。它可以显著改善浏览体验,尤其对于使用较慢网络或设备的用户。

作为 Web 开发者,理解如何针对 bfcache 优化您的页面至关重要,这样您的用户才能获得好处。

浏览器兼容性

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

bfcache 基础知识

使用后退/前进缓存(bfcache)时,当用户离开页面时,我们不会销毁页面,而是推迟销毁并暂停 JS 执行。如果用户很快后退,我们会再次使页面可见并取消暂停 JS 执行。这为用户带来了近乎即时的页面导航。

您有多少次访问一个网站并点击链接转到另一个页面,但后来意识到这不是您想要的,然后点击后退按钮?在那一刻,bfcache 可以显著提升前一页面的加载速度

启用 bfcache 的情况 系统会发起新请求以加载前一页,并且,根据该页面针对重复访问优化的程度,浏览器可能必须重新下载、重新解析和重新执行刚刚下载的某些(或全部)资源。
启用 bfcache 的情况 加载前一页是基本上即时的,因为整个页面可以从内存中恢复,而无需访问网络。

观看此 bfcache 实际运行的视频,以了解它可以为导航带来的速度提升

在后退和前进导航期间使用 bfcache 可以使页面加载速度更快。

在视频中,启用 bfcache 的示例比未启用的示例快得多。

bfcache 不仅加快了导航速度,还减少了数据使用量,因为资源不必再次下载。

Chrome 使用情况数据显示,桌面设备上十分之一的导航和移动设备上五分之一的导航是后退或前进。启用 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 中导航到另一个 URL,则先前的内容不会进入 bfcache,如果您后退,浏览器将在 iframe 内而不是在主框架内“后退”,但 iframe 内的后退导航不会使用 bfcache。

但是,当主框架从 bfcache 恢复时,嵌入的 iframe 将恢复到页面进入 bfcache 时的状态。

如果嵌入的 iframe 使用阻止 bfcache 的 API,则主框架也可能被阻止使用 bfcache。在主框架上设置的权限策略或使用 sandbox 属性 可以避免这种情况。

bfcache 和单页应用 (SPA)

由于 bfcache 适用于浏览器管理的导航,因此它不适用于单页应用 (SPA) 内的“软导航”。但是,当返回到 SPA 而不是从头开始重新完全初始化该应用时,bfcache 仍然可以提供帮助。

用于观察 bfcache 的 API

尽管 bfcache 是浏览器自动执行的优化,但开发者仍然需要知道它何时发生,以便他们可以针对它优化他们的页面,并据此调整任何指标或性能衡量

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

较新的 Page Lifecycle 事件—freezeresume—也在页面进入或离开 bfcache 时以及在其他一些情况下(例如,当后台标签页被冻结以最大限度地减少 CPU 使用率时)分派。这些事件仅在基于 Chromium 的浏览器中受支持。

观察页面何时从 bfcache 恢复

pageshow 事件在页面初始加载时和页面从 bfcache 恢复的任何时候,都在 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。但是,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 的条件,以最大限度地提高其缓存命中率,这一点至关重要。

以下部分概述了最佳实践,以尽可能使浏览器可以缓存您的页面。

永远不要使用 unload 事件

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

unload 事件对浏览器来说是有问题的,因为它早于 bfcache,并且互联网上的许多页面都在(合理的)假设下运行,即在 unload 事件触发后,页面将不再继续存在。这提出了一个挑战,因为许多页面是在假设 unload 事件会在用户离开时触发而构建的,但这不再是真的(并且很久以前就不是真的了)。

因此,浏览器面临着两难境地,他们必须在可以改善用户体验但同时也可能冒着破坏页面风险的事情之间做出选择。

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

在移动设备上,Chrome 和 Safari 将尝试缓存带有 unload 事件监听器的页面,因为损坏的风险较低,原因是 unload 事件在移动设备上一直非常不可靠。Firefox 将使用 unload 的页面视为不符合 bfcache 的条件,但在 iOS 上除外,iOS 要求所有浏览器都使用 WebKit 渲染引擎,因此它的行为类似于 Safari。

不要使用 unload 事件,而应使用 pagehide 事件。pagehide 事件在 unload 事件触发的所有情况下都会触发,并且在页面放入 bfcache 时会触发。

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

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

使用权限策略来防止在页面上使用卸载处理程序

不使用 unload 事件处理程序的网站可以确保通过使用权限策略来不添加这些处理程序。

Permissions-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 是 Web 服务器可以在响应中设置的 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 恢复过时的页面,则后退导航可能会暴露过时的信息。

另一个更关键的例子是,如果用户在公共计算机上注销网站,而下一个用户点击后退按钮。这可能会暴露用户在注销时认为已清除的私人数据。

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

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",则打开页面将引用已打开页面的窗口对象。

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

因此,最好避免创建 window.opener 引用。您可以通过尽可能使用 rel="noopener" 来做到这一点(请注意,这现在是所有现代浏览器中的默认设置)。如果您的网站需要打开窗口并通过 window.postMessage() 或直接引用窗口对象来控制它,则打开的窗口和打开器都不符合 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 DevTools 可以帮助您测试您的页面,以确保它们针对 bfcache 进行了优化,并识别可能阻止它们符合条件的任何问题。

要测试页面

  1. 在 Chrome 中导航到该页面。
  2. 在 DevTools 中,转到 Application -> Back-forward Cache
  3. 点击 Run Test 按钮。然后,DevTools 尝试离开并返回以确定页面是否可以从 bfcache 恢复。
Back-forward cache panel in DevTools
DevTools 中的 Back-forward Cache 面板。

如果测试成功,面板将报告“Restored from back-forward cache”。

DevTools reporting a page was successfully restored from bfcache
成功恢复的页面。

如果测试不成功,面板会指示原因。如果原因是您可以作为开发者解决的问题,则面板会将其标记为 Actionable

DevTools reporting failure to restore a page from 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 导航都达到 100% 的 bfcache 命中率。但是,衡量其比率可能有助于识别页面本身阻止了很大比例的后退和前进导航使用 bfcache 的情况。

Chrome 团队添加了 NotRestoredReasons API,以帮助揭示页面不使用 bfcache 的原因,以便开发人员可以提高其 bfcache 命中率。Chrome 团队还 向 CrUX 添加了导航类型,即使您不自己衡量,也可以查看 bfcache 导航的数量。

性能衡量

bfcache 也可能对 现场 收集的性能指标产生负面影响,特别是衡量页面加载时间的指标。

由于 bfcache 导航恢复现有页面而不是启动新的页面加载,因此当启用 bfcache 时,收集的页面加载总数将减少。然而,关键是,被 bfcache 恢复替换的页面加载可能是您数据集中最快的一些页面加载。这是因为根据定义,后退和前进导航是重复访问,并且重复页面加载通常比首次访问者的页面加载更快(由于 HTTP 缓存,如前所述)。

结果是您的数据集中快速页面加载次数减少,这可能会使分布向较慢的方向倾斜——尽管用户体验到的性能可能已经提高了!

有几种方法可以解决这个问题。一种方法是用它们各自的 导航类型 注释所有页面加载指标:navigatereloadback_forwardprerender。这使您可以继续在这些导航类型中监控您的性能,即使总体分布向负面倾斜。我们建议将这种方法用于非以用户为中心的页面加载指标,如 Time to First Byte (TTFB)

对于以用户为中心的指标,如 Core Web Vitals,更好的选择是报告一个更准确地代表用户体验的值。

对 Core Web Vitals 的影响

Core Web Vitals 从多个维度(加载速度、交互性、视觉稳定性)衡量用户对网页的体验,并且由于用户体验到 bfcache 恢复比完整页面加载更快,因此 Core Web Vitals 指标反映这一点非常重要。毕竟,用户并不关心是否启用了 bfcache,他们只关心导航是否快速!

收集和报告 Core Web Vitals 指标的工具,如 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 如何影响每个指标的更多详细信息,请参阅各个 Core Web Vitals 指标指南页面。有关如何实现这些指标的 bfcache 版本的具体示例,请参阅 将它们添加到 web-vitals JS 库的 PR

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

更多资源