往返缓存(简称 bfcache)是一项针对浏览器进行了优化的功能, 向后和向前导航。显著改善了浏览体验 尤其是网络或设备网速较慢的用户
作为网站开发者,您必须了解如何针对 bfcache 优化网页,以便您的用户获得这些好处。
浏览器兼容性
多年来,bfcache 在桌面和移动平台上一直受到 Firefox 和 Safari 的支持。
从版本 86 开始,Chrome 已为一小部分用户在 Android 系统上为跨网站导航启用了 bfcache。在后续版本中,我们会逐步推出更多支持。自 96 版起,我们已为桌面设备和移动设备的所有 Chrome 用户启用 bfcache。
bfcache 基础知识
bfcache 是内存中的缓存,可在用户离开网页时存储网页(包括 JavaScript 堆)的完整快照。将整个网页保存在内存中后,如果用户决定返回该网页,浏览器就能快速恢复。
您有多少次访问某个网站并点击链接前往另一网页,后来意识到这不是您想要的内容,然后点击了后退按钮?此时,bfcache 可以极大地改变前一个页面的加载速度:
未启用 bfcache | 系统会发起新的加载前一个页面的请求,根据该页面针对重复访问 优化的情况,浏览器可能需要重新下载、重新解析和执行它刚刚下载的部分(或所有)资源。 |
已启用 bfcache | 前一页面的加载基本上是即时的,因为整个页面都可以从内存中恢复,而无需连接到网络。 |
请观看这段有关 bfcache 的实际运用视频,了解它为导航带来的速度:
在该视频中,使用 bfcache 的示例比不使用 bfcache 的示例快得多。
bfcache 不仅可以加快导航速度,还可以减少流量消耗,因为无需再次下载资源。
Chrome 使用情况数据显示,1/10 的桌面版网站和 1/5 的移动用户都是返回或前进导航。启用 bfcache 后,浏览器就不必每天传输数十亿个网页的加载时间!
“缓存”是如何工作
“缓存”bfcache 使用的与 HTTP 缓存不同,后者在加速重复导航方面发挥着自身的作用。bfcache 是内存中整个页面的快照,包括 JavaScript 堆,而 HTTP 缓存仅包含之前发出的请求的响应。由于加载页面所需的全部请求都很少从 HTTP 缓存中完成,因此使用 bfcache 恢复方式的重复访问总是比优化得当的非 bfcache 导航更快。
不过,就如何最好地保留正在执行的代码而言,在内存中创建页面的快照涉及一定的复杂性。例如,当页面位于 bfcache 时,如何处理达到超时时间的 setTimeout()
调用?
答案是,浏览器会暂停 bfcache 中页面的所有待处理计时器或未解析 promise(包括 JavaScript 任务队列中的几乎所有待处理任务),并在页面从 bfcache 恢复后继续处理任务。
在某些情况下,例如对于超时和 promise,这的风险相当低,但在某些情况下,可能会导致令人困惑或出现意外行为。例如,如果浏览器暂停了作为 IndexedDB 的一部分所需的任务, 事务,则可能会影响同一源中的其他打开的标签页,因为多个标签页可以同时访问同一个 IndexedDB 数据库。因此,浏览器通常不会在 IndexedDB 事务处理过程中或使用可能会影响其他页面的 API 时尝试缓存页面。
如需详细了解各种 API 使用对网页的 bfcache 资格有何影响,请参阅针对 bfcache 优化您的网页。
bfcache 和 iframe
如果网页包含嵌入式 iframe,则 iframe 本身不符合使用 bfcache 的条件。例如,如果您在某个 iframe 中导航到了另一个页面,但之后又返回,那么浏览器会再次返回在 iframe 内而非主框架内,但 iframe 中的返回导航不会使用 bfcache。
如果嵌入式 iframe 使用阻止 bfcache 的 API,系统也可能会阻止主框架使用 bfcache。为避免出现此问题,您可以使用主框架上设置的权限政策或使用 sandbox
属性。
bfcache 和单页应用 (SPA)
由于 bfcache 适用于浏览器管理的导航,因此不适用于“软导航”在单页应用 (SPA) 中构建。不过,在返回到 SPA 时,bfcache 仍然很有用,而不是从一开始就重新对该应用进行完全重新初始化。
用于观察 bfcache 的 API
虽然 bfcache 是浏览器自动进行的优化,但开发者仍然需要了解具体发生的时间,以便针对这种情况优化其网页并调整任何指标或性能衡量结果。
用于观察 bfcache 的主要事件是页面转换事件 pageshow
和 pagehide
,大多数浏览器都支持这两个事件。
当页面进入或离开 bfcache 时,以及在一些其他情况下(例如后台标签页冻结以尽量减少 CPU 使用率时),系统也会分派较新的页面生命周期事件 freeze
和 resume
。只有基于 Chromium 的浏览器支持这些事件。
观察网页何时从 bfcache 恢复
当网页首次加载以及每次从 bfcache 恢复网页时,pageshow
事件都会在 load
事件之后立即触发。pageshow
事件具有 persisted
属性,如果网页是从 bfcache 恢复的,该属性为 true
,否则为 false
。您可以使用 persisted
属性来区分常规网页加载和 bfcache 恢复。例如:
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
console.log('This page was restored from the bfcache.');
} else {
console.log('This page was loaded normally.');
}
});
在支持 Page Lifecycle API 的浏览器中,当从 bfcache 恢复网页(紧跟在 pageshow
事件之前)以及用户重新访问已冻结的后台标签页时,会触发 resume
事件。如果您想在网页冻结后更新其状态,则可以使用 resume
事件,但如果您想衡量网站的 bfcache 命中率,则需要使用 pageshow
事件。在某些情况下,您可能需要同时使用两者。
如需详细了解 bfcache 衡量最佳做法,请参阅 bfcache 如何影响分析和性能衡量。
观察网页何时进入 bfcache
pagehide
事件会在页面卸载或浏览器尝试将其放入 bfcache 时触发。
pagehide
事件还有一个 persisted
属性。如果值为 false
,您可以确信该网页不会进入 bfcache。但是,persisted
为 true
并不能保证页面一定会缓存。这表示浏览器打算缓存网页,但可能还存在其他一些导致无法缓存的因素。
window.addEventListener('pagehide', (event) => {
if (event.persisted) {
console.log('This page *might* be entering the bfcache.');
} else {
console.log('This page will unload normally and be discarded.');
}
});
同样,如果 persisted
为 true
,freeze
事件会立即在 pagehide
事件之后触发,但这仅意味着浏览器会缓存网页。由于多种原因(下文将说明原因),它可能仍必须舍弃它。
针对 bfcache 优化网页
并非所有网页都会存储在 bfcache 中,即使某个网页确实存储在 bfcache 中,它也不会无限期地保留在 bfcache 中。开发者必须了解是什么让网页符合(和不可以)使用 bfcache,以最大限度地提高其缓存命中率。
以下部分概述了使浏览器尽可能缓存网页的最佳做法。
切勿使用 unload
事件
如需在所有浏览器中针对 bfcache 进行优化,最重要的方法是一律不使用 unload
事件。永远!
unload
事件对浏览器来说存在问题,因为它早于 bfcache,并且互联网上的许多网页都是在 unload
事件触发后网页不会继续存在的(合理)假设下运行。这就带来了一项挑战,因为其中许多网页还在构建时假设unload
事件会在用户离开网页时触发,而这已不再是事实(并且很久以来都不是这样)。
因此,浏览器面临着两难的困境,他们不得不从能够改善用户体验的方法中做出选择,但可能也存在破坏网页的风险。
在桌面设备上,Chrome 和 Firefox 已选择添加 unload
监听器,以使网页不符合使用 bfcache 的条件。这种做法的风险较低,但也会导致很多网页不符合展示条件。Safari 会尝试使用 unload
事件监听器缓存某些页面,但为了减少可能出现的中断,当用户离开网页时,它不会运行 unload
事件,这会使事件非常不可靠。
在移动设备上,Chrome 和 Safari 将尝试使用 unload
事件监听器来缓存网页,因为 unload
事件在移动设备上一直非常不可靠,导致破坏的风险较低。Firefox 会将使用 unload
的网页视为不符合使用 bfcache 的条件,但 iOS 除外。由于 iOS 需要所有浏览器使用 WebKit 呈现引擎,因此其行为类似于 Safari。
请使用 pagehide
事件,而不是 unload
事件。在 unload
事件触发的所有情况下都会触发 pagehide
事件,并且也会在将网页放入 bfcache 时触发该事件。
事实上,Lighthouse 提供了 no-unload-listeners
审核功能,该功能会在其页面上的任何 JavaScript(包括来自第三方库的 JavaScript)添加了 unload
事件监听器时向开发者发出警告。
由于它的不可靠性以及对 bfcache 的影响,Chrome 希望弃用 unload
事件。
使用权限政策防止在网页上使用卸载处理程序
不使用 unload
事件处理脚本的网站可以使用权限政策来确保不会添加这些处理脚本。
Permission-Policy: unload=()
这还可以防止第三方或扩展程序通过添加卸载处理程序并使网站不符合使用 bfcache 的条件,从而降低网站速度。
仅有条件地添加 beforeunload
监听器
beforeunload
事件不会导致您的网页不符合在现代浏览器中使用 bfcache 的条件,但以前它确实如此,并且仍然不可靠,因此除非绝对必要,否则请勿使用该事件。
不过,与 unload
事件不同,
beforeunload
。例如,您想警告用户
未保存的更改会在用户离开页面时丢失。在此示例中,
建议仅在用户未保存完毕后才添加 beforeunload
监听器
并在未保存的更改保存后立即将其移除。
window.addEventListener('beforeunload', (event) => { if (pageHasUnsavedChanges()) { event.preventDefault(); return event.returnValue = 'Are you sure you want to exit?'; } });<ph type="x-smartling-placeholder">
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); });<ph type="x-smartling-placeholder">
尽可能减少使用 Cache-Control: no-store
Cache-Control: no-store
是网络服务器可以在响应中设置的 HTTP 标头,用于指示浏览器不要将响应存储在任何 HTTP 缓存中。它用于包含敏感用户信息的资源,例如需要登录才能访问的网页。
虽然 bfcache 不是 HTTP 缓存,但过去,在网页资源本身(而不是任何子资源)上设置 Cache-Control: no-store
时,浏览器已选择不将网页存储在 bfcache 中。我们正针对以保护隐私的方式针对 Chrome 更改此行为,但目前所有使用 Cache-Control: no-store
的网页都不符合使用 bfcache 的条件。
由于 Cache-Control: no-store
会限制网页是否符合使用 bfcache 的资格,因此只应在包含敏感信息的网页上设置任何类型的缓存。
对于需要始终提供最新内容且内容不包含敏感信息的网页,请使用 Cache-Control: no-cache
或 Cache-Control: max-age=0
。这些指令会指示浏览器在提供内容前重新验证内容,并且不会影响网页的 bfcache 资格。
请注意,从 bfcache 恢复网页时,网页是从内存(而不是 HTTP 缓存)恢复的。因此,系统不会考虑 Cache-Control: no-cache
或 Cache-Control: max-age=0
等指令,并且在向用户显示内容之前不会进行任何重新验证。
不过,这仍有可能带来更好的用户体验,因为 bfcache 会即时恢复,并且网页不会在 bfcache 中保留很长时间,所以内容不太可能过期。但是,如果您的内容逐分钟发生变化,则可以使用 pageshow
事件提取任何更新,如下一部分所述。
在 bfcache 恢复后更新过时或敏感数据
如果您的网站保持了用户状态(尤其是任何敏感的用户信息),即在从 bfcache 恢复网页后,需要更新或清除相关数据。
例如,如果用户导航到结账页,然后更新了购物车,那么当从 bfcache 恢复过时网页时,返回导航可能会暴露过时的信息。
另一个更重要的例子是,用户在公共计算机上退出网站,然后下一个用户点击“后退”按钮。这可能会泄露用户认为在退出账号时已清除的隐私数据。
为避免这种情况,如果 event.persisted
为 true
,最好在 pageshow
事件发生后始终更新网页:
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// Do any checks and updates to the page
}
});
理想情况下,您会就地更新内容,但对于某些更改,您可能需要强制完全重新加载。以下代码会检查 pageshow
事件中是否存在网站专用 Cookie,如果找不到 Cookie,则会重新加载:
window.addEventListener('pageshow', (event) => {
if (event.persisted && !document.cookie.match(/my-cookie)) {
// Force a reload if the user has logged out.
location.reload();
}
});
重新加载的优点是仍然可以保留历史记录(以便向前导航),但在某些情况下,重定向可能更合适。
广告和 bfcache 恢复
您可能很想避免使用 bfcache 在每次返回/前进导航时投放一组新的广告。然而,除了会对效果产生不利影响,这种行为是否会带来更好的广告互动度也无疑是个好主意。用户可能已经注意到了他们本打算返回点击的广告,但通过重新加载而不是从 bfcache 恢复,他们却无法观看。在做出假设之前,最好通过 A/B 测试来测试这种情况。
如果网站想在 bfcache 恢复时刷新广告,然后在 event.persisted
为 true
时仅刷新 pageshow
事件中的广告,这样就能在不影响网页性能的情况下实现这一点。请与您的广告提供商联系,但可以点击此处的示例了解如何使用 Google Publishing 代码。
避免 window.opener
引用
在旧版浏览器中,如果在未指定 rel="noopener"
的情况下使用 window.open()
从带有 target=_blank
的链接中打开网页,则打开的网页将引用已打开网页的 window 对象。
除了会带来安全风险之外,包含非 null window.opener
引用的网页无法安全地放入 bfcache,因为这可能会破坏任何尝试访问它的网页。
因此,最好避免创建 window.opener
引用。您可以尽可能使用 rel="noopener"
来实现此目的(请注意,这现在是所有现代浏览器的默认设置)。如果您的网站需要打开一个窗口并通过 window.postMessage()
控制该窗口或直接引用该窗口对象,则打开的窗口和打开程序都不符合使用 bfcache 的条件。
在用户离开之前关闭打开的连接
如前所述,将网页放入 bfcache 时,它会暂停所有已安排的 JavaScript 任务,并在该网页从缓存中取出后恢复这些任务。
如果这些计划内 JavaScript 任务仅访问 DOM API(或只与当前网页隔离的其他 API),则在网页对用户不可见时暂停这些任务不会导致出现任何问题。
不过,如果这些任务连接到的 API 也可从同一源中的其他网页(例如:IndexedDB、Web Locks、WebSocket)访问,则可能会出现问题,因为暂停这些任务可能会导致其他标签页中的代码无法运行。
因此,在以下情况下,某些浏览器不会尝试将网页放入 bfcache:
- 打开 IndexedDB 连接的网页
- 正在进行 fetch() 或 XMLHttpRequest 的网页
- 具有开放的 WebSocket 或 WebRTC 连接的页面
如果您的网页使用的是上述任何 API,我们强烈建议您在 pagehide
或 freeze
事件期间关闭连接,并移除或断开连接观察器。这样,浏览器就可以安全地缓存网页,而不会有影响其他已打开的标签页的风险。
然后,如果该网页从 bfcache 恢复了,您可以在 pageshow
或 resume
事件期间重新打开或重新连接到这些 API。
以下示例展示了如何通过在 pagehide
事件监听器中关闭打开的连接来确保使用 IndexedDB 的网页符合 bfcache 资格条件:
let dbPromise;
function openDB() {
if (!dbPromise) {
dbPromise = new Promise((resolve, reject) => {
const req = indexedDB.open('my-db', 1);
req.onupgradeneeded = () => req.result.createObjectStore('keyval');
req.onerror = () => reject(req.error);
req.onsuccess = () => resolve(req.result);
});
}
return dbPromise;
}
// Close the connection to the database when the user leaves.
window.addEventListener('pagehide', () => {
if (dbPromise) {
dbPromise.then(db => db.close());
dbPromise = null;
}
});
// Open the connection when the page is loaded or restored from bfcache.
window.addEventListener('pageshow', () => openDB());
进行测试以确保页面可缓存
Chrome 开发者工具可帮助您测试网页,以确保网页已针对 bfcache 进行优化,并帮助您发现任何可能阻止网页显示的问题。
如需测试网页,请执行以下操作:
- 在 Chrome 中转到相应网页。
- 在开发者工具中,转到 Application ->往返缓存。
- 点击 Run Test 按钮。然后,DevTools 会尝试离开,并返回 以确定是否可以从 bfcache 恢复该网页。
如果测试成功,面板会显示“已从往返缓存中恢复”。
如果上传失败,面板会显示原因。如果原因在于您能作为开发者解决的问题,面板会将该问题标记为可操作。
在此示例中,使用了 unload
事件监听器会使网页不符合 bfcache 的条件。您可以通过从 unload
切换到使用 pagehide
来解决此问题:
window.addEventListener('pagehide', ...);
window.addEventListener('unload', ...);
Lighthouse 10.0 还添加了 bfcache 审核,该审核可执行类似的测试。如需了解详情,请参阅 bfcache 审核的文档。
bfcache 如何影响分析和性能衡量
如果您使用分析工具来衡量网站的访问量,可能会注意到报告的网页浏览总次数有所下降,这是因为 Chrome 为更多用户启用了 bfcache。
事实上,您可能已经在漏报其他实现了 bfcache 的浏览器所带来的网页浏览量,因为许多常用的分析库都不会将 bfcache 恢复次数作为新的网页浏览进行衡量。
若要将 bfcache 恢复次数计入网页浏览量,请为 pageshow
事件设置监听器并检查 persisted
属性。
以下示例展示了如何使用 Google Analytics 执行此操作。其他分析工具可能使用类似的逻辑:
// Send a pageview when the page is first loaded.
gtag('event', 'page_view');
window.addEventListener('pageshow', (event) => {
// Send another pageview if the page is restored from bfcache.
if (event.persisted) {
gtag('event', 'page_view');
}
});
衡量 bfcache 命中率
您可能还需要衡量是否使用了 bfcache,以帮助识别没有使用 bfcache 的网页。这可以通过衡量网页加载的导航类型来实现:
// Send a navigation_type when the page is first loaded.
gtag('event', 'page_view', {
'navigation_type': performance.getEntriesByType('navigation')[0].type;
});
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// Send another pageview if the page is restored from bfcache.
gtag('event', 'page_view', {
'navigation_type': 'back_forward_cache';
});
}
});
根据 back_forward
次导航和 back_forward_cache
次导航的计数计算 bfcache 命中率。
请务必注意,在网站所有者控制的很多情况下,返回/前进导航都不会使用 bfcache,其中包括:
- 当用户退出浏览器并重新启动时触发
- 当用户复制标签页时触发
- 当用户关闭并重新打开标签页时触发
在某些情况下,某些浏览器可能会保留原始导航类型,因此可能会显示 back_forward
类型(尽管这些类型不是返回/前进导航)。
即使没有这些排除项,bfcache 也会在一段时间后被舍弃,以节省内存。
因此,网站所有者不应期望所有 back_forward
导航都能达到 100% 的 bfcache 命中率。不过,衡量两者的比率对于确定在很大一部分向后和向前导航中,网页本身阻止了 bfcache 的使用。
Chrome 团队添加了 NotRestoredReasons
API,以帮助揭示网页不使用 bfcache 的原因,以便开发者提高其 bfcache 命中率。Chrome 团队还在 CrUX 中添加了导航类型,这样一来,您即使不自行衡量,也可以查看 bfcache 导航的数量。
效果衡量
bfcache 还可能会对该字段中收集的性能指标(特别是衡量网页加载时间的指标)产生负面影响。
由于 bfcache 导航会恢复现有页面而不是启动新页面加载,因此启用 bfcache 后,收集的页面加载总数会减少。但重要的是,由 bfcache 恢复所取代的网页加载可能是数据集中速度最快的网页之一。这是因为根据定义,后退和前进导航是重复访问,而重复网页加载通常比初访者的网页加载快(如前所述,得益于 HTTP 缓存的原因)。
结果就是,数据集中的快速页面加载更少,这可能会减缓分布速度,尽管用户体验的性能很可能已经得到提升!
您可以通过以下几种方式解决此问题。一种是为所有网页加载指标分别添加相应的导航类型注解:navigate
、reload
、back_forward
或 prerender
。这样一来,即使总体分布偏差为负,您也能继续监控在这些导航类型中的效果。对于非以用户为中心的网页加载指标(例如首字节时间 (TTFB)),我们建议使用此方法。
对于核心网页指标等以用户为中心的指标,更好的选择是报告能够更准确地反映用户体验的值。
对核心网页指标的影响
Core Web Vitals 会从多个维度(加载速度、互动、视觉稳定性)衡量网页的用户体验,由于用户会体验到 bfcache 恢复的速度比完整网页加载快,因此核心网页指标指标必须反映这一点。毕竟,用户不关心 bfcache 是否启用了,他们只关心导航是否快速!
用于收集和报告 Core Web Vitals 指标的工具(例如 Chrome 用户体验报告)会在其数据集中将 bfcache 恢复视为单独的网页访问。虽然没有专用的 Web 性能 API 可以在 bfcache 恢复后衡量这些指标,但您可以使用现有的 Web API 来估算它们的值:
- 对于 Largest Contentful Paint (LCP),请使用
pageshow
事件的时间戳和下一个绘制帧的时间戳之间的增量,因为系统会同时绘制该帧中的所有元素。在 bfcache 恢复时,LCP 和 FCP 相同。 - 对于 Interaction to Next Paint (INP),请继续使用现有的 Performance Observer,但将当前的 INP 值重置为 0。
- 对于 Cumulative Layout Shift (CLS),请继续使用现有的 Performance Observer,但将当前的 CLS 值重置为 0。
如需详细了解 bfcache 如何影响各个指标,请参阅各个 Core Web Vitals 指标指南页面。如需通过具体示例了解如何实现这些指标的 bfcache 版本,请参阅 PR 将这些指标添加到 web-vitals JS 库中。
web-vitals JavaScript 库在其报告的指标中支持 bfcache 恢复。
其他资源
- Firefox 缓存 (Firefox 中的 bfcache)
- 页面缓存 (Safari 中的 bfcache)
- 往返缓存:网页暴露行为 (不同浏览器之间的 bfcache 差异)
- bfcache 测试人员 (测试不同的 API 和事件对浏览器中的 bfcache 的影响)
- 性能改变者:浏览器往返缓存 (来自《Smashing》杂志的案例研究,展示了通过启用 bfcache 对核心网页指标的显著改进)