【译】点击浏览器的前进后退按钮时,页面的缓存机制

简介: 【译】点击浏览器的前进后退按钮时,页面的缓存机制

640.png

这是一篇译文


原文标题: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存在的时间一样长,并且在当今使用的几乎所有浏览器中都受支持。

新的页面生命周期事件 freezeresume 也会在页面进入或离开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,因为这可能会破坏任何试图访问它的页面。


译者注:安全问题可以参考本公众号的这篇文章 :


使用<a>标签时,你可能会忽略的一个安全问题


因此,最好使用 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。

相关文章
|
13天前
|
Linux Go iOS开发
怎么禁用 vscode 中点击 go 包名时自动打开浏览器跳转到 pkg.go.dev
本文介绍了如何在 VSCode 中禁用点击 Go 包名时自动打开浏览器跳转到 pkg.go.dev 的功能。通过将 gopls 的 `ui.navigation.importShortcut` 设置为 &quot;Definition&quot;,可以实现仅跳转到定义处而不打开链接。具体操作步骤包括:打开设置、搜索 gopls、编辑 settings.json 文件并保存更改,最后重启 VSCode 使设置生效。
38 7
怎么禁用 vscode 中点击 go 包名时自动打开浏览器跳转到 pkg.go.dev
|
3月前
|
Web App开发 JavaScript 前端开发
添加浮动按钮点击滚动到网页底部的纯JavaScript演示代码 IE9、11,Maxthon 1.6.7,Firefox30、31,360极速浏览器7.5.3.308下测试正常
添加浮动按钮点击滚动到网页底部的纯JavaScript演示代码 IE9、11,Maxthon 1.6.7,Firefox30、31,360极速浏览器7.5.3.308下测试正常
|
4月前
|
XML 缓存 JSON
为什么浏览器中有些图片、PDF等文件点击后有些是预览,有些是下载
为什么浏览器中有些图片、PDF等文件点击后有些是预览,有些是下载
292 0
|
1月前
|
存储 缓存 网络协议
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点,GET、POST的区别,Cookie与Session
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点、状态码、报文格式,GET、POST的区别,DNS的解析过程、数字证书、Cookie与Session,对称加密和非对称加密
|
1月前
|
域名解析 缓存 网络协议
浏览器中输入URL返回页面过程(超级详细)、DNS域名解析服务,TCP三次握手、四次挥手
浏览器中输入URL返回页面过程(超级详细)、DNS域名解析服务,TCP三次握手、四次挥手
|
6月前
|
Web App开发 XML 开发框架
技术心得记录:在IE浏览器中的奇怪页面表现
技术心得记录:在IE浏览器中的奇怪页面表现
69 0
|
4月前
|
网络协议 前端开发 JavaScript
浏览器加载网页的幕后之旅:从URL到页面展示详解
【8月更文挑战第31天】当在浏览器地址栏输入URL并回车后,一系列复杂过程随即启动,包括DNS解析、TCP连接建立、HTTP请求发送、服务器请求处理及响应返回,最后是浏览器页面渲染。这一流程涉及网络通信、服务器处理和客户端渲染等多个环节。通过示例代码,本文详细解释了每个步骤,帮助读者深入理解Web应用程序的工作机制,从而在开发过程中作出更优决策。
77 5
|
4月前
|
JavaScript
js怎样控制浏览器前进、后退、页面跳转
js怎样控制浏览器前进、后退、页面跳转
64 0
|
5月前
|
Web App开发 编解码
软件开发常见流程之兼容性和手机屏页面设计,PC端和移动端常见浏览器,国内的UC都是根据Webkit修改过来的内核,开发重点关注尺寸,常见移动端尺寸汇总,移动端,理想视口根据你设别的样式进行修改
软件开发常见流程之兼容性和手机屏页面设计,PC端和移动端常见浏览器,国内的UC都是根据Webkit修改过来的内核,开发重点关注尺寸,常见移动端尺寸汇总,移动端,理想视口根据你设别的样式进行修改
|
4天前
|
存储 缓存 NoSQL
解决Redis缓存数据类型丢失问题
解决Redis缓存数据类型丢失问题
118 85