往返缓存(或 bfcache)是一种浏览器优化,可实现即时后退和前进导航。它可以显著提升浏览体验,尤其是对于网络或设备速度较慢的用户。
作为 Web 开发者,了解如何针对 bfcache 优化网页至关重要,以便用户能够获享其优势。
浏览器兼容性
所有主流浏览器都包含 bfcache,包括 Chrome 96 及更高版本、Firefox 和 Safari。
bfcache 基础知识
借助返回/前进缓存 (bfcache),我们会推迟销毁网页,并暂停 JS 执行,而不是在用户离开时销毁网页。如果用户很快返回,我们会让网页重新显示并取消暂停 JS 执行。这会为用户带来近乎即时的网页导航体验。
您有多少次访问某个网站并点击链接前往另一个网页,但发现该网页不是您想要的,然后点击返回按钮?在这种情况下,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
事件会触发。如果您想在网页冻结后更新其状态(包括 bfcache 中的网页),可以使用 resume
事件;但如果您想衡量网站的 bfcache 命中率,则需要使用 pageshow
事件。在某些情况下,您可能需要同时使用这两种方法。
如需详细了解 bfcache 衡量最佳实践,请参阅 bfcache 对分析和效果衡量的影响。
观察网页何时进入 bfcache
当网页卸载或浏览器尝试将其放入 bfcache 时,都会触发 pagehide
事件。
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 的条件,以便尽可能提高缓存命中率。
以下部分概述了一些最佳实践,可尽可能提高浏览器缓存网页的可能性。
切勿使用 unload
事件
在所有浏览器中针对 bfcache 进行优化的最重要的方法是,切勿使用 unload
事件。永远!
unload
事件对浏览器来说存在问题,因为它早于 bfcache,并且互联网上的许多网页都基于合理的假设(即网页在 unload
事件触发后不会继续存在)运行。这带来了挑战,因为许多此类网页在构建时也假定在用户导航离开时,系统会触发 unload
事件,但这已不再正确(而且已经很长时间不正确了)。
因此,浏览器面临着两难抉择,必须在可以改善用户体验但也可能导致网页损坏的做法之间做出选择。
在桌面设备上,Chrome 和 Firefox 选择了以下做法:如果网页添加了 unload
监听器,则不符合 bfcache 使用条件。这样做虽然风险较小,但也使许多网页不符合条件。Safari 会尝试使用 unload
事件监听器缓存某些网页,但为了减少潜在的故障,它不会在用户导航离开时运行 unload
事件,这使得该事件非常不可靠。
在移动设备上,由于 unload
事件在移动设备上一直非常不可靠,因此 Chrome 和 Safari 会尝试缓存具有 unload
事件监听器的网页,因为这样可以降低损坏风险。Firefox 会将使用 unload
的网页视为不符合 bfcache 条件,但在 iOS 上除外,因为 iOS 要求所有浏览器都使用 WebKit 渲染引擎,因此其行为与 Safari 类似。
请改用 pagehide
事件,而不是 unload
事件。只要触发 unload
事件,就会触发 pagehide
事件,并且在将网页放入 bfcache 时也会触发 pagehide
事件。
事实上,Lighthouse 提供了 no-unload-listeners
审核,如果网页上的任何 JavaScript(包括来自第三方库的 JavaScript)添加了 unload
事件监听器,系统会向开发者发出警告。
由于 unload
事件不可靠且会对 bfcache 性能产生影响,Chrome 打算弃用 unload
事件。
使用权限政策来阻止在网页上使用取消加载处理脚本
不使用 unload
事件处理程序的网站可以使用权限政策来确保不会添加这些处理程序。
Permissions-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?'; } });
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); });
尽可能减少使用 Cache-Control: no-store
Cache-Control: no-store
是一种 HTTP 标头,Web 服务器可以在响应中设置此标头,以指示浏览器不要将响应存储在任何 HTTP 缓存中。它适用于包含敏感用户信息的资源,例如登录后才能访问的页面。
虽然 bfcache 不是 HTTP 缓存,但在过去,当在网页资源本身(而不是任何子资源)上设置 Cache-Control: no-store
时,浏览器会选择不将网页存储在 bfcache 中,因此使用 Cache-Control: no-store
的任何网页都可能不符合 bfcache 的条件。我们正在努力以可保护隐私的方式更改 Chrome 的此行为。
由于 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 发布商代码执行此操作。
避免使用 window.opener
引用
在旧版浏览器中,如果使用 window.open()
通过包含 target=_blank
的链接打开网页,而未指定 rel="noopener"
,则打开网页会引用打开网页的窗口对象。
除了存在安全风险之外,具有非 null window.opener
引用的网页也无法安全地放入 bfcache,因为这可能会破坏尝试访问它的所有网页。
因此,最好避免创建 window.opener
引用。为此,您应尽可能使用 rel="noopener"
(请注意,这现在是所有现代浏览器的默认值)。如果您的网站需要打开窗口并通过 window.postMessage()
或直接引用窗口对象来控制它,则打开的窗口和打开窗口的操作都无法使用 bfcache。
在用户离开之前关闭打开的连接
如前所述,当网页保留在 bfcache 中时,它会暂停所有已安排的 JavaScript 任务,并在网页从缓存中移出时恢复这些任务。
如果这些安排的 JavaScript 任务仅访问 DOM API 或仅限于当前网页的其他 API,那么在网页对用户不可见时暂停这些任务不会造成任何问题。
不过,如果这些任务连接到可从同一源的其他网页访问的 API(例如 IndexedDB、Web 锁、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 中,前往相应网页。
- 在 DevTools 中,依次选择应用 -> 往返缓存。
- 点击运行测试按钮。然后,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
导航的 bfcache 命中率为 100%。不过,衡量此比率有助于确定哪些网页本身会阻止在大量返回和前进导航中使用 bfcache。
Chrome 团队添加了 NotRestoredReasons
API,以帮助揭示网页不使用 bfcache 的原因,以便开发者提高 bfcache 命中率。Chrome 团队还向 CrUX 添加了导航类型,这样一来,您无需自行衡量,也能查看 bfcache 导航次数。
效果衡量
bfcache 还可能会对在现场收集的性能指标产生负面影响,尤其是衡量网页加载时间的指标。
由于 bfcache 导航会恢复现有网页,而不是发起新的网页加载,因此启用 bfcache 后,收集的网页加载总数会减少。不过,重要的是,被 bfcache 恢复项替换的网页加载可能原本是数据集中最快的网页加载。这是因为,按定义,返回和前进导航属于重复访问,而重复网页加载通常比首次访问者的网页加载更快(如前所述,这是由于 HTTP 缓存)。
结果是,数据集中的快速页面加载次数会减少,这可能会导致分布偏差更慢,尽管用户获得的性能可能有所提升!
您可以通过以下几种方法解决此问题。一种是使用相应的导航类型(navigate
、reload
、back_forward
或 prerender
)为所有网页加载指标添加注释。这样,即使总体分布偏向负面,您也可以继续监控这些导航类型的效果。对于收到第一个字节的时间 (TTFB) 等非以用户为中心的网页加载指标,我们建议使用此方法。
对于以用户为中心的指标(例如 Core Web Vitals),更好的做法是报告更准确地反映用户体验的值。
对 Core Web Vitals 的影响
核心网页指标可衡量用户在多种维度(加载速度、互动性、视觉稳定性)方面的网页体验。由于用户会发现 bfcache 恢复比完整网页加载更快,因此核心网页指标必须反映这一点。毕竟,用户并不关心是否启用了 bfcache,他们只关心导航速度!
收集和报告 Core Web Vitals 指标的工具(例如 Chrome 用户体验报告)会在其数据集中将 bfcache 恢复视为单独的网页访问。虽然在 bfcache 恢复后没有专门用于衡量这些指标的 Web 性能 API,但您可以使用现有的 Web API 来近似估算其值:
- 对于 Largest Contentful Paint (LCP),请使用
pageshow
事件的时间戳与下一个绘制帧的时间戳之间的差值,因为帧中的所有元素将同时绘制。对于 bfcache 恢复,LCP 和 FCP 相同。 - 对于 Interaction to Next Paint (INP),请继续使用现有的性能观察器,但将当前 INP 值重置为 0。
- 对于 Cumulative Layout Shift (CLS),请继续使用现有的效果观察器,但将当前 CLS 值重置为 0。
如需详细了解 bfcache 对每个指标的影响,请参阅各个 Core Web Vitals 指标指南页面。如需查看如何实现这些指标的 bfcache 版本的具体示例,请参阅将这些指标添加到 web-vitals JS 库的 PR。
web-vitals JavaScript 库在其报告的指标中支持 bfcache 恢复。
其他资源
- Firefox 缓存 (Firefox 中的 bfcache)
- 网页缓存 (Safari 中的 bfcache)
- 往返缓存:网站公开的行为 (不同浏览器之间的 bfcache 差异)
- bfcache 测试工具 (测试不同 API 和事件对浏览器中的 bfcache 有何影响)
- 性能变革者:浏览器返回/前进缓存 (Smashing Magazine 中的一项案例研究,展示了启用 bfcache 后 Core Web Vitals 指标显著提升的情况)