15

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

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzU4Mzc4NzI2NA%3D%3D&%3Bmid=2247484748&%3Bidx=1&%3Bsn=135567c9b7364aa3e1322150c11cea18
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

mQn2yeB.png!mobile

这是一篇译文

原文标题: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侦听器,然后在保存未保存的更改后立即将其删除。

下面的写法是:x:的(无条件的监听了beforeunload事件):

window.addEventListener('beforeunload', (event) => {
if (pageHasUnsavedChanges()) {
event.preventDefault();
return event.returnValue = 'Are you sure you want to exit?';
}
});

下面的写法是:white_check_mark:的:

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。

E3mQnu.jpg!mobile

福利彩蛋:这篇文章是本公众号的第100篇文章。感谢各位支持。我准备了一个彩蛋,欢迎各位移步本公众号的下一篇文章。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK