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

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

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。

相关文章
|
2月前
|
存储 缓存 安全
第二章 HTTP请求方法、状态码详解与缓存机制解析
第二章 HTTP请求方法、状态码详解与缓存机制解析
|
12天前
|
前端开发 安全 UED
【项目实战】从终端到浏览器:实现 ANSI 字体在前端页面的彩色展示
在学习和工作中,我们经常需要使用日志来记录程序的运行状态和调试信息。而为了更好地区分不同的日志等级,我们可以使用不同的颜色来呈现,使其更加醒目和易于阅读。 在下图运行结果中,我们使用了 colorlog 库来实现彩色日志输出。通过定义不同日志等级对应的颜色,我们可以在控制台中以彩色的方式显示日志信息。例如,DEBUG 级别的日志使用白色,INFO 级别的日志使用绿色,WARNING 级别的日志使用黄色,ERROR 级别的日志使用红色,CRITICAL 级别的日志使用蓝色。
|
5天前
|
Web App开发 XML 开发框架
技术心得记录:在IE浏览器中的奇怪页面表现
技术心得记录:在IE浏览器中的奇怪页面表现
|
2月前
|
缓存 安全 Java
7张图带你轻松理解Java 线程安全,java缓存机制面试
7张图带你轻松理解Java 线程安全,java缓存机制面试
|
14天前
|
缓存 调度
Eureka的注册表拉取及多级缓存机制简析
Eureka的注册表拉取及多级缓存机制简析
18 2
|
2月前
|
域名解析 存储 缓存
【域名解析DNS专栏】DNS缓存机制详解:如何提升域名解析速度
【5月更文挑战第21天】本文探讨了DNS缓存机制的原理及优化方法。DNS缓存是存储已解析域名与IP地址的临时数据库,能减少网络延迟,减轻服务器负担并提升用户体验。优化策略包括增加缓存容量,设置合理过期时间,使用智能DNS服务及定期清理缓存。文中还提供了一个Python示例,展示如何通过缓存提升域名解析速度。
【域名解析DNS专栏】DNS缓存机制详解:如何提升域名解析速度
|
4天前
|
SQL 缓存 Java
Java框架之MyBatis 07-动态SQL-缓存机制-逆向工程-分页插件
Java框架之MyBatis 07-动态SQL-缓存机制-逆向工程-分页插件
|
2月前
|
存储 缓存 运维
【Docker 专栏】Docker 镜像的分层存储与缓存机制
【5月更文挑战第8天】Docker 镜像采用分层存储,减少空间占用并提升构建效率。每个镜像由多个层组成,共享基础层(如 Ubuntu)和应用层。缓存机制加速构建和运行,通过检查已有层来避免重复操作。有效管理缓存,如清理无用缓存和控制大小,可优化性能。分层和缓存带来资源高效利用、快速构建和灵活管理,但也面临缓存失效和层管理挑战。理解这一机制对开发者和运维至关重要。
【Docker 专栏】Docker 镜像的分层存储与缓存机制
|
7天前
|
存储 缓存 NoSQL
如何在Java中实现高效的缓存机制
如何在Java中实现高效的缓存机制
|
7天前
|
存储 缓存 NoSQL
如何在Java中实现缓存机制?
如何在Java中实现缓存机制?