对浏览器的后退、前进缓存

对浏览器的后退、前进缓存

后退 / 前进缓存(或 bfcache)是一种浏览器优化,可实现即时后退导航。它极大地改善了用户的浏览体验,尤其是那些网络或设备速度较慢的用户。

作为 Web 开发人员,了解如何在所有浏览器中为 bfcache 优化页面非常重要,这样你的用户才能获得更好的体验。

浏览器兼容性

Firefox 和 Safari 在桌面和移动设备上都支持 bfcache 已有 很多年了。

从版本 86 开始,Chrome 启用了 bfcache 以便在 Android 中为一小部分用户提供 跨站点导航。在 Chrome 87 中,bfcache 支持将在所有 Android 用户中进行跨站点导航,以期在不久的将来也支持 同站点导航。

bfcache 基础

bfcache 是一种内存中缓存,用于在用户离开时存储页面的完整快照(包括 JavaScript 堆)。将整个页面存储在内存中,如果用户决定返回,浏览器可以快速轻松地恢复页面。

你访问了多少次网站并单击链接以转到另一页,只是意识到这不是你想要的,然后单击 “后退” 按钮?在那一刻,bfcache 可以极大地改变上一页的加载速度:

没有启用 bfcache 启动一个新请求以加载上一页,并且,根据该页面为重复访问而 优化的程度,浏览器可能必须重新下载,重新解析和重新执行某些(或全部)资源。它刚刚下载。
随着 bfcache 启用 加载上一页实际上是即时的,因为可以从内存中还原整个页面,而根本不必访问网络。

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

Chrome 的使用情况数据显示,台式机中十分之一的导航是前进或后退,五分之一是移动的。启用 bfcache,浏览器可以消除每天的数据传输和数十亿网页加载时间!

缓存如何在工作?

bfcache 使用的 “缓存” 与 HTTP 缓存不同 (HTTP 缓存在加速重复导航中也很有用)。bfcache 是内存中整个页面的快照(包括 JavaScript 堆),而 HTTP 缓存仅包含先前发出的请求的响应。由于很难通过 HTTP 缓存满足加载页面所需的所有请求,因此使用 bfcache 还原进行重复访问始终比最优化的非 bfcache 导航要快。

但是,在内存中创建页面的快照在如何最好地保留进行中的代码方面涉及一些复杂性。例如,setTimeout()当页面位于 bfcache 中时,如何处理 到达超时的调用?

答案是浏览器会暂停运行任何待处理的计时器或未解决的 Promise(实际上是 JavaScript 任务队列中的所有待处理任务),并在(或是否)从 bfcache 恢复页面时恢复处理任务。

在某些情况下,这是相当低的风险(例如,超时或承诺),但在其他情况下,则可能导致非常混乱或意外的行为。例如,如果浏览器暂停了 IndexedDB 事务所需的任务,则它可能会影响同一来源的其他打开的选项卡(因为相同的 IndexedDB 数据库可以同时被多个选项卡访问)。因此,浏览器通常不会尝试在 IndexedDB 事务中间或使用可能影响其他页面的 API 缓存页面。

有关各种 API 使用情况如何影响页面的 bfcache 资格的更多详细信息,请参阅下面的针对 bfcache 优化页面。

API 来观察 bfcache

虽然 bfcache 是浏览器自动进行的优化,但对于开发人员来说,知道它何时发生仍然很重要,以便他们可以为其优化页面并相应地调整任何指标或性能度量 。

观察 bfcache 的主要事件是页面转换事件(pageshow 和)pagehide,这些事件与 bfcache 一样长,并且在当今使用的几乎所有浏览器中都得到支持。

当页面进入或离开 bfcache 以及其他情况时,也会分派较新的 Page Lifecycle 事件。例如,当冻结背景标签以最小化 CPU 使用率时。请注意,当前仅在基于 Chromium 的浏览器中支持 Page Lifecycle 事件。freeze``resume

当一个页面从 bfcache 恢复观察

pageshow事件触发后权load当页面初始加载和页面从 bfcache 恢复的任何时间的事件。该pageshow 事件具有一个 persisted 属性,该 属性将是true从 bfcache 还原页面的情况(false如果不是)。你可以使用该persisted属性来区分常规页面加载与 bfcache 还原。例如:

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

在支持 Page Lifecycle API 的浏览器中,resume从 bfcache 还原页面时(紧接pageshow 事件之前),该事件也会触发,尽管当用户重新访问冻结的背景选项卡时,也会触发该事件。如果要在页面冻结后恢复页面状态(包括 bfcache 中的页面),可以使用该resume事件,但是如果要测量站点的 bfcache 命中率,则需要使用该pageshow事件。在某些情况下,你可能需要同时使用两者。

优化你的 bfcache 网页

并非所有页面都存储在 bfcache 中,即使页面确实存储在其中,它也不会无限期地停留在其中。开发人员了解什么使页面符合(或不符合)bfcache 的条件以最大化其缓存命中率,这一点至关重要。

以下各节概述了最佳实践,以使浏览器尽可能地缓存你的页面。

永远不要使用unload事件

在所有浏览器中优化 bfcache 的最重要方法是从不使用该unload事件。曾经!

unload事件对浏览器来说是有问题的,因为它早于 bfcache,并且 Internet 上的许多页面都在(合理的)假设下运行,该假设是unload事件触发后页面将不继续存在。这就提出了一个挑战,因为其中许多页面也是基于这样的假设而建立的:该unload事件将在用户导航离开时触发,而这不再成立(并且很长一段时间没有成立)。

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

Firefox 选择了使页面不具备 bfcache 资格(如果它们添加了unload 侦听器),这样风险较低,但也取消了许多页面的资格。Safari 会尝试使用unload事件侦听器缓存某些页面,但是为了减少潜在的损坏,unload当用户离开时,Safari 不会运行该事件。

由于 Chrome 中 65%的页面注册了unload事件侦听器,以便能够缓存尽可能多的页面,因此 Chrome 选择将实现与 Safari 保持一致。

代替使用unload事件,而使用pagehide事件。该pagehide 事件触发在所有的情况下,unload事件触发当前,它 当一个页面被放在 bfcache 闪光。

实际上,Lighthouse v6.2.0 已经添加了一个 no-unload-listeners audit,它将警告开发人员其页面上的任何 JavaScript(包括第三方库中的 JavaScript)是否添加了unload事件监听器。

警告: 切勿添加unload事件监听器!请改用pagehide事件。添加unload事件侦听器会使你的网站在 Firefox 中变慢,并且该代码甚至大部分时间都不会在 Chrome 和 Safari 中运行。

beforeunload事件不会使你的页面在 Chrome 或 Safari 中不符合 bfcache 的条件,但会使其在 Firefox 中不符合 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需要时添加侦听器(而在不需要时将其删除)。

避免使用 window.opener 引用

在某些浏览器(包括基于 Chromium 的浏览器)中,如果使用 window.open() 或(在 88 版本之前的基于 Chromium 的浏览器中)通过未 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:

  • 未完成 IndexedDB 事务的页面
  • 正在进行 fetch()或 XMLHttpRequest 的页面
  • 具有打开的 WebSocket 或 WebRTC 连接的页面

如果你的页面使用这些 API 中的任何一个,则最好始终在pagehideorfreeze事件期间关闭连接并删除或断开观察者的连接。这样,浏览器就可以安全地缓存页面,而不会影响其他打开的选项卡。

然后,如果从 bfcache 还原了页面,则可以重新打开或重新连接到这些 API(pageshowresume事件中)。

使用上面列出的 API 不会使页面失去存储在 bfcache 中的资格,只要它们在用户离开之前没有被积极使用。但是,有些 API(嵌入式插件,辅助程序,广播频道和其他一些 API )当前的使用情况确实使页面无法被缓存。尽管 Chrome 最初在 bfcache 的初始版本中故意保守一些,但长期目标是使 bfcache 尽可能多地使用 API。

测试以确保你的网页缓存

虽然无法确定页面在卸载时是否已放入缓存中,但可以断言后退或前进导航确实从缓存中恢复了页面。

当前,在 Chrome 中,页面最多可以在 bfcache 中保留三分钟,这应该有足够的时间来运行测试(使用 Puppeteer 或 WebDriver 之类的工具 ),以确保事件的persisted 属性pageshowtrue在离开页,然后单击返回按钮。

请注意,在正常情况下,页面应在缓存中保留足够长的时间以运行测试,但可以随时将其静默退出(例如,如果系统内存不足)。测试失败并不一定意味着你的页面不可缓存,因此你需要配置测试或相应地建立失败标准。

如果你不希望将页面存储在 bfcache 中,则可以通过将Cache-Control顶级页面响应上的标头设置为来确保不缓存该页面 no-store

Cache-Control: no-store

所有其他缓存指令(包括no-cache甚至no-store在子帧上)都不会影响页面使用 bfcache 的资格。

尽管此方法有效且可在浏览器中使用,但它还具有其他缓存和性能隐患,这可能是不希望的。为了解决这个问题,有人建议添加一个更明确的退出机制,包括一种在需要时清除 bfcache 的机制(例如,当用户从共享设备上退出网站时)。