前端学堂
学有所用

浏览器前进后退的缓存机制

浏览器的前进后退功能大家经常使用,但是这里面涉及到的浏览器缓存机制,你可能并不了解,一起看下吧。

前进后退缓存(或bfcache)是一种浏览器优化,可实现即时后退和前向导航。它显著改善了用户的浏览体验,特别是那些网络或设备较慢的用户。

作为网页开发人员,了解如何在所有浏览器上优化页面以获得bfcache至关重要,这样您的用户就可以收获好处。

浏览器兼容性

多年来,FirefoxSafari都支持bfcache,跨越桌面和移动版。

从86版开始,Chrome为一小部分用户启用了在Android上跨站点导航的bfcache。在Chrome 87中,bfcache支持将扩展到所有Android用户,用于跨站点导航,意图在不久的将来也支持同站点导航。

bfcache 基础知

bfcache是一个内存缓存,在用户导航时存储页面的完整快照(包括JavaScript堆)。整个页面都在内存中,如果用户决定返回,浏览器可以快速轻松地恢复它。

你访问过多少次一个网站,点击链接进入另一个页面,却发现它不是你想要的,然后点击后退按钮?在那一刻,bfcache可以大大改变前一页加载的速度:

启用bfcache 启动新请求以加载前一页,根据该页面的多次访问优化程度,浏览器可能必须重新下载、重新解析和重新执行它刚刚下载的部分(或全部)资源。
启用bfcache 加载上一页基本上即时的,因为整个页面可以从内存中恢复,根本不需要进入网络

bfcache不仅加快了导航速度,还减少了数据使用,因为资源不必再次下载。

Chrome使用数据显示,桌面上每10个导航中就有1个,移动中每5个导航中就有1个是后退或向前。启用bfcache后,浏览器可以消除每天数十亿网页的数据传输和加载时间!

“缓存”是如何工作的

bfcache使用的“缓存”与HTTP缓存不同(HTTP缓存在加快重复导航方面也很有用)。bfcache是内存中整个页面(包括JavaScript堆)的快照,而HTTP缓存仅包含对先前请求的响应。由于加载页面所需的所有请求都可以从HTTP缓存中完成,因此使用bfcache还原的重复访问也总是比优化得最好的非bfcache导航更快。

然而,在内存中创建页面快照涉及如何最好地保存进行中的代码。例如,当您页面处于bfcache中时,如何处理setTimeout()调用,其中超时已达到该调用?

答案是,浏览器暂停运行任何挂起计时器或未解析的承诺——基本上是JavaScript任务队列中的所有挂起任务——并在页面从bfcache恢复时(或如果)恢复处理任务。

在某些情况下,这是相当低的风险(例如,超时或承诺),但在其他情况下,它可能导致非常混乱或意外的行为。例如,如果浏览器暂停作为IndexedDB事务的一部分所需的任务,它可能会影响同一来源的其他打开选项卡(因为相同的IndexedDB数据库可以同时被多个选项卡访问)。因此,浏览器通常不会试图在索引数据库事务中间缓存页面或使用可能影响其他页面的API。

有关各种 API 使用如何影响页面 bfcache 资格的更多详细信息,请参阅下面的“优化页面”以获取 bfcache。

观察bfcache的API

虽然bfcache是浏览器自动进行的优化,但开发人员知道它何时发生仍然很重要,这样他们就可以为它优化页面,并相应地调整任何指标或性能测量。

用于观察bfcache的主要事件是页面的生命周期事件—pageshow 和 pagehide—在如今所有支持bfcache的浏览器中都支持这两个事件。

还有俩个新的生命周期事件是—freeze 和 resume— 这两个事件只有在Chromium基础的浏览器中存在。

观察何时从bfcache恢复页面

The pageshow event fires right after the load event when the page is initially loading and any time the page is restored from bfcache. The pageshow event has a persisted property which will be true if the page was restored from bfcache (and false if not). You can use the persisted property to distinguish regular page loads from bfcache restores. For example:

window.addEventListener('pageshow', function(event) {
  if (event.persisted) {
    console.log('This page was restored from the bfcache.');
  } else {
    console.log('This page was loaded normally.');
  }
});

In browsers that support the Page Lifecycle API, the resume event will also fire when pages are restored from bfcache (immediately before the pageshow event), though it will also fire when a user revisits a frozen background tab. If you want to restore a page's state after it's frozen (which includes pages in the bfcache), you can use the resume event, but if you want to measure your site's bfcache hit rate, you'd need to use the pageshow event. In some cases, you might need to use both.

有关bfcache测量最佳做法更多详细信息,请参阅性能和分析的影响

观察页面何时进入bfcach

The pagehide event is the counterpart to the pageshow event. The pageshow event fires when a page is either loaded normally or restored from the bfcache. The pagehide event fires when the page is either unloaded normally or when the browser attempts to put it into the bfcache.

The pagehide event also has a persisted property, and if it's false then you can be confident a page is not about to enter the bfcache. However, if the persisted property is true, it doesn't guarantee that a page will be cached. It means that the browser intends to cache the page, but there may be factors that make it impossible to cache.

window.addEventListener('pagehide', function(event) {
  if (event.persisted === true) {
   console.log('This page *might* be entering the bfcache.');
  } else {
    console.log('This page will unload normally and be discarded.');
  }
});

Similarly, the freeze event will fire immediately after the pagehide event (if the event's persisted property is true), but again that only means the browser intends to cache the page. It may still have to discard it for a number of reasons explained below.

优化您的页面以使用bfcach

并非所有页面都存储在bfcache中,即使页面确实存储在那里,它也不会无限期地保存在那里。开发人员必须了解是什么让页面符合(和不符合条件)的bfcache,以最大限度地提高其缓存命中率。

以下各节概述了使浏览器尽可能缓存页面的最佳做法。

不能用unload事件

The most important way to optimize for bfcache in all browsers is to never use the unload event. Ever!

The unload event is problematic for browsers because it predates bfcache and many pages on the internet operate under the (reasonable) assumption that a page will not continue to exist after the unload event has fired. This presents a challenge because many of those pages were alsobuilt with the assumption that the unload event would fire any time a user is navigating away, which is no longer true (and hasn't been true for a long time).

因此,浏览器面临着两难境地,他们必须在可以改善用户体验的东西中进行选择,但也可能会破坏页面。

Firefox has chosen to make pages ineligible for bfcache if they add an unload listener, which is less risky but also disqualifies a lot of pages. Safari will attempt to cache some pages with an unload event listener, but to reduce potential breakage it will not run the unload event when a user is navigating away.

Since 65% of pages in Chrome register an unload event listener, to be able to cache as many pages as possible, Chrome chose to align implementation with Safari.

Instead of using the unload event, use the pagehide event. The pagehide event fires in all cases where the unload event currently fires, and it also fires when a page is put in the bfcache.

In fact, Lighthouse v6.2.0 has added a no-unload-listeners
audit
, which will warn developers if any JavaScript on their pages (including that from third-party libraries) adds an unload event listener.

Warning: Never add an unload event listener! Use the pagehide event instead. Adding an unload event listener will make your site slower in Firefox, and the code won't even run most of the time in Chrome and Safari.

Only add beforeunload listeners conditionally

The beforeunload event will not make your pages ineligible for bfcache in Chrome or Safari, but it will make them ineligible in Firefox, so avoid using it unless absolutely necessary.

Unlike the unload event, however, there are legitimate uses for beforeunload. For example, when you want to warn the user that they have unsaved changes they'll lose if they leave the page. In this case, it's recommended that you only add beforeunload listeners when a user has unsaved changes and then remove them immediately after the unsaved changes are saved.

不要

window.addEventListener('beforeunload', (event) => {
  if (pageHasUnsavedChanges()) {
    event.preventDefault();
    return event.returnValue = 'Are you sure you want to exit?';
  }
});
The code above adds a beforeunload listener unconditionally.

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.
onPageHasUnsavedChanges(() => {
  window.addEventListener('beforeunload', beforeUnloadListener);
});

// A function that invokes a callback when the page's unsaved changes are resolved.
onAllChangesSaved(() => {
  window.removeEventListener('beforeunload', beforeUnloadListener);
});
The code above only adds the beforeunload listener when it's needed (and removes it when it's not).

避免 window.opener 引用

在某些浏览器(包括基于Chromium的浏览器)中,如果页面是使用window.open()打开的或者(在版本88之前的基于Chromium的浏览器中)从target=_blank的链接打开——但没有指定rel="noopener",则打开的页面将引用打开页面的窗口对象。

In addition to being a security risk, a page with a non-null window.opener reference cannot safely be put into the bfcache because that could break any pages attempting to access it.

As a result, it's best to avoid creating window.opener references by using rel="noopener"whenever possible. If your site requires opening a window and controlling it through window.postMessage() or directly referencing the window object, neither the opened window nor the opener will be eligible for bfcache.

在用户导航离开之前,请务必关闭打开的连接

如上所述,当页面放入bfcache时,所有计划中的JavaScript任务都会暂停,然后在页面从缓存中取出时恢复。

如果这些计划中的JavaScript任务只访问DOM API——或仅隔离在当前页面的其他API——那么当用户看不到页面时暂停这些任务不会造成任何问题。

然而,如果这些任务连接到API,这些API也可以从同一来源的其他页面访问(例如:IndexedDB、Web Locks、WebSockets等),这可能是有问题的,因为暂停这些任务可能会阻止其他选项卡中的代码运行。

因此,在以下情况下,大多数浏览器不会尝试将页面放入bfcache中:

If your page is using any of these APIs, it's best to always close connections and remove or disconnect observers during the pagehide or freeze event. That will allow the browser to safely cache the page without the risk of it affecting other open tabs.

Then, if the page is restored from the bfcache, you can re-open or re-connect to those APIs (in the pageshow or resume event).

使用上述API不会取消页面存储在bfcache中的资格,只要它们在用户导航离开之前没有积极参与使用。然而,在有些API(嵌入式插件、工人、广播频道和其他几个API)的使用目前确实使页面无法缓存。虽然Chrome在首次发布bfcache时故意保守,但长期目标是让bfcache与尽可能多的API一起工作。

测试以确保您的页面可缓存

虽然无法确定页面在卸载时是否被放入缓存,但可以断言反向或向前导航确实从缓存中恢复了页面。

Currently, in Chrome, a page can remain in the bfcache for up to three minutes, which should be enough time to run a test (using a tool like Puppeteer or WebDriver) to ensure that the persistedproperty of a pageshow event is true after navigating away from a page and then clicking the back button.

请注意,虽然在正常情况下,页面应在缓存中停留足够长的时间以运行测试,但它可以随时被静默驱逐(例如,如果系统处于内存压力下)。失败的测试并不一定意味着您的页面不可缓存,因此您需要相应地配置测试或构建失败条件。

明白了!在Chrome中,bfcache目前仅在手机上启用。要在桌面上测试bfcache,您需要启用#back-forward-cache标志

选择退出bfcache的方法

If you do not want a page to be stored in the bfcache you can ensure it's not cached by setting the Cache-Control header on the top-level page response to no-store:

Cache-Control: no-store

All other caching directives (including no-cache or even no-store on a subframe) will not affect a page's eligibility for bfcache.

虽然这种方法很有效,可以在浏览器中工作,但它还有其他可能不可取的缓存和性能影响。为了解决这个问题,有人建议添加一个更明确的退出机制,包括一个在需要时清除bfcache的机制(例如,当用户在共享设备上退出网站时)。

此外,在Chrome中,用户级退出目前可以通过#back-forward-cache标志,以及基于企业政策退出

注意事项:鉴于bfcache提供的用户体验明显更好,除非出于隐私原因,例如用户在共享设备上退出网站,否则不建议选择退出。

bfcache如何影响分析和性能测量

如果您使用分析工具跟踪对网站的访问量,您可能会注意到,随着Chrome继续为更多用户启用bfcache,报告的页面浏览量总数有所下降。

事实上,您可能已经少报了来自其他实现bfcache的浏览器的页面浏览量,因为大多数流行的分析库不会跟踪bfcache恢复作为新的页面浏览量。

If you don't want your pageview counts to go down due to Chrome enabling bfcache, you can report bfcache restores as pageviews (recommended) by listening to the pageshow event and checking the persisted property.

以下示例演示了如何使用谷歌分析来做到这一点;其他分析工具的逻辑应该相似:

// 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缓存,如前所述)。

结果是数据集中快速的页面加载更少,这可能会使分发速度变慢——尽管用户体验的性能可能有所改善!

There are a few ways to deal with this issue. One is to annotate all page load metrics with their respective navigation type: navigate, reload, back_forward, or prerender. This will allow you to continue to monitor your performance within these navigation types—even if the overall distribution skews negative. This approach is recommended for non-user-centric page load metrics like Time to First Byte (TTFB).

对于以用户为中心的指标,如Core Web Vitals,更好的选择是报告一个更准确地表示用户体验的值。

Caution: The back_forward navigation type in the Navigation Timing API is not to be confused with bfcache restores. The Navigation Timing API only annotates page loads, whereas bfcache restores are re-using a page loaded from a previous navigation.

赞(1) 打赏
一分也是爱,觉得好请我喝杯咖啡吧!前端学堂 » 浏览器前进后退的缓存机制

一分也是爱,觉得好请我喝杯咖啡吧!

支付宝扫一扫打赏

微信扫一扫打赏