浏览器的前进后退时,页面的缓存机制
后退/前进缓存(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,因为这可能会破坏任何试图访问它的页面。
译者注:安全问题可以参考本公众号的这篇文章 :
因此,最好使用 rel="noopener" 来保证打开的页面无法引用之前的页面。如果你的站点需要打开一个新页面并通过window.postMessage()或直接引用之前的window对象来控制之前的窗口,则新打开的页面和之前的页面都不符合bfcache的条件。
总是在用户导航离开之前关闭打开的连接
如上文所述,当一个页面被放入bfcache时,所有预定的JavaScript任务都将暂停,然后在页面从缓存中取出时继续执行。
如果这些计划的JavaScript任务只访问dom api或其他与当前页面隔离的api,那么用户看不到页面时,暂停这些任务不会导致任何问题。
但是,如果这些任务涉及到和其他页面有关的api(例如:IndexedDB、Web Locks、WebSockets等),则可能会出现问题,因为暂停这些任务可能会阻止其他选项卡中的代码运行。因此,在以下情况下,大多数浏览器不会尝试将页面放入bfcache:
•页面有未完成的indexdb事物•页面有进行中的fetch和ajax请求•页面具有打开的WebSocket或WebRTC的连接
如果您的页面正在使用这些api中的任何一个,那么最好在pagehide或freeze事件期间始终关闭连接并移除或断开观察者。这将允许浏览器安全地缓存页面,而不会影响其他打开的选项卡。
然后,如果页面从bfcache恢复,则可以重新打开或重新连接到这些api(在pageshow或resume事件中)。
测试以确保页面可缓存
虽然无法确定页面在卸载时是否已放入缓存,但可以断言:后退或前进导航确实从缓存中还原了页面。
目前,在Chrome中,一个页面可以在bfcache中停留3分钟,这应该足够运行一个测试(使用puppeter或WebDriver等工具),以确保pageshow事件的persistent属性在离开页面并单击back按钮后为true。
请注意,虽然在正常情况下,页面应该在缓存中保留足够长的时间来运行测试,但可以随时以静默方式将其逐出(例如,如果系统处于内存压力下)。失败的测试并不一定意味着页面不可缓存,因此您需要相应地配置测试或构建失败标准。
退出bfcache的方法
如果不希望页面存储在bfcache中,可以通过将顶级页面响应中的Cache-Control头设置为no-store来确保该页面不会被缓存:
Cache-Control: no-store
所有其他缓存指令(包括子frame中的 no-cache 甚至 no-store)不会影响页面bfcache的资格。
虽然这个方法是有效的,并且可以跨浏览器工作,但是它还有其他缓存和性能影响。为了解决这个问题,有人提议添加一个更明确的退出机制,包括在需要时清除bfcache的机制(例如,当用户从共享设备上的网站注销时)。
在Chrome中,可以通过设置#back-forward-cache
标志位来实现,或者一个企业级的证书。
bfcache如何影响分析和性能度量
如果你用分析工具跟踪你的网站访问量,你可能会注意到报告的页面浏览量减少了,因为Chrome继续为更多用户启用bfcache。
事实上,您可能已经低估了其他实现bfcache的浏览器的页面浏览量,因为大多数流行的分析库都不会将bfcache恢复作为新的页面视图进行跟踪。
如果不希望由于启用Chrome的bfcache而导致pv数下降,可以通过监听pageshow事件并检查persistent属性,将bfcache还原报告为pv。
例如:
// Send a pageview when the page is first loaded.
gtag('event', 'page_view')
window.addEventListener('pageshow', function(event) {
if (event.persisted === true) {
// Send another pageview if the page is restored from bfcache.
gtag('event', 'page_view')
}
});
性能度量
bfcache还可能对字段中收集的性能指标产生负面影响,特别是度量页面加载时间的指标。
由于bfcache导航还原现有页面而不是启动新页面加载,因此启用bfcache时收集的页面加载总数将减少。但是,关键的是,由bfcache恢复替换的页面加载可能是数据集中最快的页面加载。这是因为根据定义,后退和前进导航是重复访问,并且重复页面加载通常比首次访问的页面加载快(由于前面提到的HTTP缓存)。
结果是数据集中的快速页面加载更少,这可能会使分布更慢,尽管用户体验到的性能可能已经提高!
有几种方法可以解决这个问题。一种方法是用各自的导航类型注释所有页面加载度量:导航、重新加载、后退向前或预加载。这将允许您继续监视这些导航类型中的性能,即使总体分布为负。对于非以用户为中心的页面加载指标,如第一个字节的时间(TTFB),建议使用这种方法。
对web核心指标的影响
web核心指标衡量用户在不同维度(加载速度、交互性、视觉稳定性)对网页的体验,由于用户体验到bfcache恢复比传统页面加载更快的导航,因此核心Web Vitals指标反映这一点很重要。毕竟,用户并不关心bfcache是否被启用,他们只关心导航是否快速!
Chrome用户体验报告(Chrome User Experience Report)等工具将很快更新,以将bfcache恢复视为数据集中单独的页面访问。
虽然在bfcache恢复后还没有专门的web性能api来度量这些指标,但是可以使用现有的web api来近似计算它们的值。
•对于最大内容绘制(LCP),可以使用pageshow事件的时间戳和下一个绘制帧的时间戳之间的增量(因为帧中的所有元素都将同时绘制)。请注意,对于bfcache还原,LCP和FCP(首帧内容绘制)是相同的。•对于First Input Delay(FID),可以在pageshow事件中重新添加事件监听器(与FID polyfill使用的监听器相同),并将FID报告为bfcache还原后第一个输入的延迟。•对于累积布局移位(CLS),您可以继续使用现有的Performance Observer;你只需将当前的CLS值重置为0。
最后
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)
欢迎加我微信「qianyu443033099」拉你进技术群,长期交流学习...
关注公众号「前端下午茶」,持续为你推送精选好文,也可以加我为好友,随时聊骚。