这是一篇译文
原文标题:Back/forward cache
原文链接:https://web.dev/bfcache/
后退/前进缓存(Back/forward cache, 以下简称bfcache)是一种浏览器优化,可实现即时的后退和前进导航。它显著改善了用户的浏览体验,尤其是那些网络或设备速度较慢的用户。
作为web开发人员,了解如何在所有浏览器上基于bfcache优化页面非常重要。这样可以提高用户体验。
浏览器兼容性
Firefox和Safari都早已支持bfcache,包括桌面和移动设备。
从86版开始,Chrome已经为一小部分用户启用了Android上的跨站点导航。在chrome87中,bfcache支持将推广到所有Android用户进行跨站点导航,目的是在不久的将来也支持相同的站点导航。
bfcache基础知识
bfcache是一个内存中的缓存,它在用户离开时存储页面的完整快照(包括JavaScript堆)。由于整个页面都在内存中,如果用户决定返回,浏览器可以快速轻松地恢复页面。
有多少次你访问一个网站,点击一个链接进入另一个页面,却发现这不是你想要的,然后点击后退按钮?此时,bfcache对上一页的加载速度会有很大的影响:
不支持bfc时:将启动一个新的请求来加载上一个页面,并且,根据该页面针对重复访问的优化程度,浏览器可能需要重新下载、重新解析和重新执行刚下载的部分(或全部)资源。
开启了bfc时:加载上一个页面基本上是即时的,因为整个页面可以从内存中恢复,而不必访问网络。
bfcache不仅加快了导航速度,还减少了数据使用,因为不必再次下载资源。
Chrome的使用数据显示,桌面上十分之一的导航和手机上五分之一的导航要么后退要么前进。启用bfcache后,浏览器可以消除每天数十亿个网页的数据传输和加载时间!
cache是如何工作的
bfcache使用的“缓存”不同于HTTP缓存(这在加速重复导航方面也很有用)。bfcache是内存中整个页面的快照(包括JavaScript堆),而HTTP缓存只包含以前发出的请求的响应。由于加载页面所需的所有请求都能从HTTP缓存中得到满足的情况非常罕见,因此使用bfcache恢复进行的重复访问总是比最优化的非bfcache导航更快。
然而,在内存中创建页面快照是有一定复杂性的,特别是涉及到如何最好地保存正在进行的代码。例如,当页面在bfcache中时,如何处理到达超时的setTimeout()调用?
答案是,浏览器暂停运行任何挂起的计时器或未resolved的Promise(实际上是JavaScript任务队列中所有挂起的任务),并在页面从bfcache恢复时(或如果)恢复处理任务。
在某些情况下,暂停任务是低风险的(例如,超时或Promise),但在其他情况下,它可能会导致非常混乱或意外的行为。例如,如果浏览器暂停IndexedDB事务中所需的任务,它可能会影响同一源中打开的其他选项卡(因为多个选项卡可以同时访问同一个IndexedDB数据库)。因此,浏览器通常不会尝试在IndexedDB事务中间缓存页面,也不会使用可能影响其他页面的api。
有关各种API用法如何影响页面的bfcache的详细信息,请参考下文的内容。
监听bfcache的API
虽然bfcache是浏览器自动进行的一种优化,但对于开发人员来说,知道何时发生这种情况仍然很重要,这样他们就可以针对bfcache优化自己的页面,并相应地调整任何指标或性能度量。
用于观察bfcache的主要事件是页面转换事件pageshow和pagehide,这两个事件存在的时间和bfcache存在的时间一样长,并且在当今使用的几乎所有浏览器中都受支持。
新的页面生命周期事件 freeze 和 resume 也会在页面进入或离开bfcache时以及在其他一些情况下触发。例如,当后台选项卡冻结以最小化CPU使用率时。注意,页面生命周期事件目前仅在基于Chromium的浏览器中受支持。
监听页面从bfc中恢复
当页面最初加载时,pageshow事件在load事件之后立即激发。另外,页面从bfcache还原时,pageshow也会触发。pageshow事件有一个persisted属性,如果从bfcache还原页面,则该属性为true;如果不是,则为false。您可以使用persisted属性来区分常规页面加载和bfcache还原。例如:
- window.addEventListener('pageshow', function(event) {
- if (event.persisted === true) {
- // 页面从bfc中恢复
- console.log('This page was restored from the bfcache.');
- } else {
- // 页面正常加载
- console.log('This page was loaded normally.');
- }
- });
在支持页面生命周期API的浏览器中,当页面从bfcache还原时(就在pageshow事件之前),resume事件也会触发,不过当用户重新访问冻结的背景选项卡时,它也会触发。如果要在冻结页面(包括bfcache中的页面)后恢复页面状态,可以使用freeze事件,但如果要测量站点的bfcache命中率,则需要使用pageshow事件。在某些情况下,您可能需要同时使用这两种方法。
监听页面进入bfc
pagehide事件是pageshow事件的对应项。当页面正常加载或从bfcache还原时,将激发pageshow事件。pagehide事件在页面正常卸载或浏览器试图将其放入bfcache时触发。
pagehide事件还有一个persistent属性,如果它是false,那么您可以确信页面不会进入bfcache。但是,如果persistent属性为true,则不能保证将缓存页。这意味着浏览器打算缓存页面,但可能有一些因素导致无法缓存。
- window.addEventListener('pagehide', function(event) {
- if (event.persisted === true) {
- // 页面可能会进入bfc缓存
- console.log('This page *might* be entering the bfcache.');
- } else {
- // 页面会正常退出,并且会被丢弃
- console.log('This page will unload normally and be discarded.');
- }
- });
类似地,freeze事件将在pagehide事件之后立即触发(如果事件的persistent属性为true),但这同样意味着浏览器打算缓存页面。在下面描述的情况下,它可能仍然必须丢弃它。
为bfcache优化页面
并不是所有的页面都存储在bfcache中,即使页面确实存储在那里,它也不会无限期地停留在那里。开发人员必须了解是什么使页面符合bfcache的条件(和不符合条件),以最大限度地提高缓存命中率。
下面几节概括了使浏览器尽可能缓存页面的最佳实践。
不要使用 unload 事件
在所有浏览器中优化bfcache的最重要方法是永远不要使用unload事件。
unload事件对于浏览器来说是有问题的,因为它早于bfcache触发,并且网络上的许多页面都是在一个(合理的)假设下运行的:unload事件触发后,页面将不再存在了。这就带来了一个挑战,因为许多页面的构建都是基于这样一个假设:unload事件将在用户离开时触发。然而,事实已经不是这样了(而且在很长一段时间内都不是这样)。
译者注:这里我的理解是,浏览器在设计unload事件之初,就是在页面不需要的时候触发。如果开发者监听了unload事件,则表示页面销毁时需要执行一些逻辑,这个时候,页面自然是不需要再进行缓存了。然而这种情况下,很多开发者是希望页面被缓存的,这和unload事件本身的含义有冲突。
所以浏览器面临着一个两难的选择,他们必须在能改善用户体验的同时也可能有破坏页面的风险。
Firefox选择了如果添加unload侦听器,那么页面就不符合bfcache的条件,这样做风险较小,但也会使很多页面无法bfc。Safari会尝试缓存一些监听了unload事件的页面,但是为了减少潜在的破坏,当用户导航离开时,Safari不会触发unload事件。
由于Chrome中65%的页面都注册了unload事件侦听器,为了能够缓存尽可能多的页面,Chrome选择与Safari保持一致。
不要使用unload事件,使用pagehide事件。pagehide事件在unload事件触发的所有情况下都会触发,并且在页面放入bfcache时也会触发。
注意,永远不要添加unload事件侦听器!请改用pagehide事件。在Firefox中添加unload事件监听器会使你的站点变慢,而代码在Chrome和Safari中大部分时间都不会运行。
仅仅有条件的添加 beforeunload 事件
beforeunload事件不会使您的页面不符合Chrome或Safari的bfcache,但在Firefox中不行,因此除非绝对必要,否则请避免使用它。
但是,与unload事件不同,beforeunload有合法的用法。例如,当您要警告用户他们有未保存的更改时,如果他们离开页面,他们将丢失。在这种情况下,建议仅在用户有未保存的更改时添加beforeunload侦听器,然后在保存未保存的更改后立即将其删除。
下面的写法是?的(无条件的监听了beforeunload事件):
- window.addEventListener('beforeunload', (event) => {
- if (pageHasUnsavedChanges()) {
- event.preventDefault();
- return event.returnValue = 'Are you sure you want to exit?';
- }
- });
下面的写法是?的:
- 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.
- // 当页面内容未保存时,才监听beforeunload事件
- onPageHasUnsavedChanges(() => {
- window.addEventListener('beforeunload', beforeUnloadListener);
- });
- // A function that invokes a callback when the page's unsaved changes are resolved.
- // 当页面内容保存完毕时,移除beforeunload事件
- onAllChangesSaved(() => {
- window.removeEventListener('beforeunload', beforeUnloadListener);
- });
避免window.opener的references
在一些浏览器中(包括chrome,从86版本起),如果使用 window.open或者 target=_blank 打开一个新的页面,但是没有写明:rel="noopener",那么,新打开的页面中,会包含一个对原有页面的引用。
除了存在安全风险外,保留了对其他页面引用的页面不能安全地放入bfcache,因为这可能会破坏任何试图访问它的页面。
译者注:安全问题可以参考本公众号的这篇文章 :