往返缓存(BFCache)优化页面载入

使用浏览器的前进后退 按钮时,优化页面以实现即时加载

BFCache是一种浏览器优化,可实现即时前进和后退载入页面。它改善了用户的浏览体验,尤其是那些网络或设备速度较慢的用户。

浏览器兼容性

多年以来,FirefoxSafari 一直在桌面移动设备 上支持BFCache

Chrome ,从86版本 开始为一小部分用户在Android 上启用了BFCache跨站点导航。至96版本 起,桌面和移动设备上的Chrome 用户已经能支持BFCache

BFCache基础知识

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

有多少次你访问某个网站,并单击某个链接跳转到另一个页面,却发现这并不是你想要的页面,然后单击后退按钮。在那一刻,BFCache可以对前一个页面的加载速度产生很大的影响:

是否启用BFCache 状况
未启用 发起一个新的网络请求加载上一个页面,根据页面重复访问的优化程度,可能会重新下载、解析、执行部分(全部)资源
启用 加载上一个页面是即时的,因为整个页面可以从内存中恢复,根本不需要访问网络

观看BFCache的实际操作视频,了解它可以为导航带来的速度提升

BFCache不仅可以加快导航速度,还可以减少数据使用,因为它避免了再次下载资源。

Chrome 使用数据显示,桌面上十分之一的导航和移动设备上五分之一的导航是后退或前进。启用BFCache后,浏览器可以消除每天数十亿个网页的数据传输和加载时间。

BFCache如何工作

BFCache缓存HTTP缓存 不同。 BFCache在内存中缓存整个页面的(包括Javascript堆)快照,而HTTP缓存仅包含先前发出请求的响应。由于加载页面所需的所有请求都能从HTTP缓存中满足的情况非常罕见,因此使用BFCache恢复重复访问的页面,始终比最优化的非BFCache导航更快。

在内存中缓存整个页面的快照,涉及到如何更好的缓存正在执行的代码。例如,当页面处于BFCache中时,如何处理setTimeout超时的调用?

浏览器会暂停 执行任何挂起的计时器以及Promise后续处理任务(Javascript事件队列中所有任务都被暂停执行),在页面从BFCache恢复时恢复处理任务。

在某些情况下,可能会导致意外的行为。例如:如果浏览器暂停IndexedDB事务所需的任务,则可能会影响同一源中其他打开的Tab页面,因为多个Tab页面可以同时访问相同的IndexDB数据库。

浏览器通常不会尝试在IndexedDB事务中缓存页面或使用可能影响其他页面的API。

用于观察BFCache的API

虽然BFCache是浏览器自动执行的优化,但对开发人员来说,了解它何时发生仍然很重要。以便他们可以针对它优化页面,并相应的调整任何指标或性能测量。

用于观察BfCache的主要事件是pageshowpagehide,在现今使用的几乎所有浏览器中都支持。

当页面进入或离开时,也会调度较新的页面生命周期事件:freezeresume。页面生命周期事件,目前仅基于Chromium的浏览器支持。

观察页面何时从BFCache恢复

当页面最初加载或从BFCache恢复页面时,pageshow 事件在load 事件后触发。pageshow 事件对象有一个persisted 属性。该属性值为true,页面从BFCache恢复(为false不是)。

可以使用persisted 属性来区分常规页面加载和从BFCache恢复。例如:

js 复制代码
window.addEventListener('pageshow', (event) => {
    if(event.persisted){
        // 从BFCache恢复
    } else {
        // 常规页面加载
    }
});

观察页面何时进入BFCache

当页面正常卸载或浏览器尝试将其放入BFCache时,会触发pagehide 事件。pagehide 事件对象也有一个persisted 属性。如果该属性的值为false,则可以确信该页面不会进入BFCache。如果该属性的值为true,则不能保证页面将被缓存。意味着浏览器打算缓存该页面,但可能存在一些因素导致无法缓存。

js 复制代码
window.addEventListener('pagehide', (event) => {
    if(event.persisted){
        // 不能保证一定放入BFCache
    } else {
        // 一定未放入BFCache
    }
});

针对BFCache优化页面

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

以下总结了使浏览器尽可能缓存你页面的最佳实践。

切勿使用unload事件

在所有浏览器中优化BFCache的最重要方法是永远不要使用unload事件。

该事件对于浏览器来说是有问题的,因为它早于BFCache。并且互联网上的许多页面都是在假设下运行的,即触发unload事件后页面将不会继续存在。

不使用unload 事件,而是使用pagehide 事件。pagehide 事件在unload 事件之前触发。在移动设备上,unload事件不存在或极其不可靠。

实际上,Lighthouse有一项no-unload-listeners审核。如果开发人员页面上的任何Javascript(包括来自第三方库的Javascript)添加了unload事件监听器,它将向开发人员发出警告。

仅有条件地添加beforeunload事件监听

beforeunload 事件不会使你在ChromeSafari 中不符合BFCache的条件,但会使在Firefox 中不符合BFCache的条件。因此除非绝对必要,否则请避免使用它。

当你想要警告用户他们有未保存的更改时,如果他们离开页面,就会丢失。在这种情况下,建议你仅在用户未保存更改时添加beforeunload事件监听,然后在保存未保存的更改后立即删除它们。

js 复制代码
// beforeunload处理程序
const beforeUnloadHandler = (event) => {

};

// 页面变更未保存,添加beforeunload事件监听
onPageHasUnsavedChange(() => {
    window.addEventListener('beforeunload', beforeUnloadHandler);
});

// 页面变更保存,移除beforeunload事件监听
onAllChangeSaved(() => {
    window.addEventListener('beforeunload', beforeUnloadHandler);
});

尽量减少使用Cache-Control: no-store

Cache-Control: no-store是Web服务器在响应头上设置的标头,指示浏览器不要将响应存储在任何HTTP缓存中。应用于包含敏感用户信息的资源,例如:登录后的页面。

当在页面资源本身上设置Cache-Control: no-store时,浏览器选择不将页面存储在BFCache中。目前Chrome 正在开展工作,以保护隐私的方式改变这种行为,但目前任何使用Cache-Control: no-store的页面都没有BFCache的资格。

Cache-Control: no-store限制了页面使用BFCache的资格。因此只能在包含敏感信息的页面上设置它,在这些页面上任何类型的缓存都不合适。

对于希望始终提供最新内容且该内容不包含敏感信息的页面,请使用Cache-Control: no-cacheCache-Control: max-age=0。这些指令指示浏览器在提供内容之前重新向服务器验证内容,并且它们不会影响页面的BFCache资格。

请注意,当从BFCache恢复页面时,它是从内存恢复,而不是从HTTP缓存恢复。因此不会考虑Cache-Control: no-cacheCache-Control: max-age=0之类的指令,并且在向用户显示内容之前,也不会发生向服务器重新验证。

这依然可能是更好的用户体验,因为BFCache恢复是即时的,并且页面不会在BFCache中保留很长时间,因此内容不太可能过时。如果你的内容确实是每分钟发生变化,你可以使用pageshow事件获取任何更新(下一节所述)。

BFCache恢复后更新陈旧或敏感数据

如果你的站点保留用户状态(尤其是任何敏感的用户信息),则在从BFCache恢复页面后需要更新或清除数据。

例如,如果用户导航到结账页面,然后更新其购物车。则如果从BFCache恢复过时的页面,后退导航可能会暴露过时的信息。

另一个更关键的示例,如果用户在公共计算机上退出站点,而下一个用户单击后退按钮。这可能会暴露用户在注销时假设已清除的私人数据。

为了避免这种情况,最好在事件pageshow 中,如果event.persistedtrue,则始终更新页面。

load 事件与pageshow 事件触发的顺序:load => pageshow 。意味着当触发pageshow事件时,页面已载入。

以下代码检查是否存在特定于站点的cookie,如果未找到,则重新加载:

js 复制代码
window.addEventListener('pageshow', (event) => {
    if(event.persisted && !document.cookie.match(/my-cookie/)){
        // 如果用户退出登录,在HTTP缓存的基础上,强制更新。
        location.reload();
    }
})

避免window.opener引用

使用window.open(url, '_blank')打开一个新的窗口页面,则被打开的窗口页面将引用该窗口的window对象。

除了存在安全风险之外,具有非空window.opener引用的页面,还不能安全地放入BFCache中。因为这可能会破坏尝试访问它的任何页面。

如果你的站点需要打开一个窗口,并通过直接引用窗口对象来控制它。则打开的窗口和当前窗口都没有资格使用BFCache

在用户离开之前始终关闭打开的链接

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

如果这些计划的Javascript任务仅访问DOM API(或仅与当前页面隔离的其他API),那么在用户看不到页面时,暂停这些任务不会导致任何问题。

如果这些任务连接到的API也可从同源的其他页面访问(例如:IndexedDB、Web Locks、WebSockets等),则可能会出现问题。因为暂停这些任务可能会阻止其他Tab页面中的代码运行。

因此,在以下情况下,某些浏览器不会尝试将页面放入BFCache中:

  • 具有打开的indexedDB连接的页面。
  • 正在进行fetchXMLHttpRequest的页面。
  • 具有开放WebSocketWebRTC连接的页面。

如果你的页面正在使用这些API中的任何一个,则在pagehidefreeze事件期间,最好始终关闭连接,并删除或断开观察者。这将允许浏览器安全地缓存页面,而不会影响其他打开的Tab页面。

如果页面从BFCache恢复,你可以在pageshowresume事件期间,重新打开或重新连接到这些API。

以下示例显示了在pagehide 事件期间,通过关闭indexedDB 打开的连接,来确保你的页面在使用indexedDB 时,符合BFCache的条件。

js 复制代码
let indexedDBPromise = null;

const openIndexedDB = () => {
    if(indexedDBPromise) return indexedDBPromise;
    
    indexedDBPromise = new Promise((resolve, reject) => {
        const req = indexedDB.open('my-db', 1);
        req.onupgradeneeded = () => req.result.createObjectStore('keyvalue');
        req.onerror = () => reject(req.error);
        req.onsuccess = () => resolve(req.result);
    });
};

const closeIndexedDB = () => {
    if(indexedDBPromise){
        indexedDBPromise.then(db => db.close());
        indexedDBPromise = null;
    }
}

// 离开页面,断开indexedDB连接
window.addEventListener('pagehide', closeIndexedDB);
// 从BFCache恢复或首次加载,建立IndexedDB连接
window.addEventListener('pageshow', openIndexedDB);

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

Chrome DevTool 可以帮助你测试页面,以确保它们针对BFCache进行了优化,并识别可能阻止它们符合条件的任何问题。

要测试特定页面,请在Chrome 中导航到该页面,然后在DevTools 中转到Application => Back-forward Cache 。接下来单击"运行测试"按钮,DevTools 将尝试导航离开并返回以确定是否可以从BFCache恢复页面。

如果成功,面板将报告"已从往返缓存恢复":

如果不成功,面板将指示页面未恢复并列出原因。如果原因是你作为开发人员可以解决的,也会指出:

unload 在上面的屏幕截图中,事件监听器的使用,导致页面无法获得BFCache。你可以通过将unload 换成pagehide来解决此问题。

Lighthouse 10后还添加了BFCache审核,它执行与DevTools类似的测试,并且还提供了审核失败时页面不合格的原因。

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

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

事实上,你可能已经少报了其他实现BFCache的浏览器的浏览量,因为大多数流行的分析库不会将BFCache恢复,跟踪为新的浏览量。

如果你不希望你的综合浏览量因Chrome 启用BFCache而下降,你可以通过pageshow 事件对象中的persisted属性,将BFCache恢复报告为综合浏览量。

js 复制代码
// 仅在页面首次加载时执行,页面从BFCache恢复,不会执行
gtag('event', 'page_view');  
  
window.addEventListener('pageshow', (event) => {  
     // 页面加载或从BFCache恢复,都会触发pageshow事件
    if (event.persisted) {  
        gtag('event', 'page_view');  
    }  
});

需要注意的是,页面从BFCache恢复,是从内存中直接恢复,无需网络请求,无需重新解析和执行JS代码,仅恢复Javascript事件队列中的任务。会触发pageshowresume 事件,不会触发loadvisibilitychange事件。

测量你的BFCache命中率

你可能还希望跟踪是否使用了BFCache,以帮助识别未使用BFCache的页面。

js 复制代码
window.addEventListener('pageshow', (event) => {  
    const navigationType = performance.getEntriesByType('navigation')[0].type;
    const { persisted } = event;
    if (persisted || navigationType == 'back_forward' ) {  
        gtag('event', 'back_forward_navigation', {  
        'isBFCache': persisted,  
        }); 
    }  
});

在网站所有者控制之外的许多情况下,后退或前进导航不会使用BFCache,包括:

  • 当用户退出浏览器并再次启动时。
  • 当用户复制Tab页(选项卡)时。
  • 当用户关闭选项卡并取消关闭时。

即使没有这些排除,BFCache也会在一段时间后被丢弃以节省内存。

网站所有者不应期望所有back_forward导航都有100%的BFCache命中率。测量它们的比率,有助于识别页面本身阻止BFCache使用高比例的后退和前进导航的页面。

Chrome团队正在开发一个NotRestoreReasons API,以帮助揭示未使用BFCache的原因,以帮助开发人员了解未使用缓存的原因,以及他们是否可以为此改进网站。

性能测量

BFCache还会对现场收集的性能指标产生负面影响,特别是测量页面加载时间的指标。

由于BFCache导航恢复现有页面而不是启动新页面加载,因此启用BFCache时收集的页面加载总数将会减少。

重要的是,被BFCache恢复所取代的页面加载可能是数据集中最快的页面加载之一。这是因为根据定义,前进和后退导航是重复访问,并且重复页面加载通常比首次访问者的页面加载更快(由于HTTP缓存)。

结果是数据集中的快速页面加载更少,这可能会使分布变得更慢------尽管用户体验到的性能可能有所提高。

有几种方法可以解决这个问题。一种是使用各自的导航类型来注释所有页面加载指标:navigatereloadback_forwardprerender。这将使您能够继续监控这些导航类型中的表现 - 即使整体分布偏向负数。对于非以用户为中心的页面加载指标(例如首字节时间 (TTFB))建议使用此方法。

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

请勿将导航计时 APIback_forward中的导航类型与BFCache 恢复混淆。导航计时 API 仅注释页面加载,而 BFCache 恢复则重复使用从先前导航加载的页面。

相关推荐
耶啵奶膘39 分钟前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^2 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie3 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic3 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿4 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具4 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161775 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test5 小时前
js下载excel示例demo
前端·javascript·excel
Yaml45 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事5 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro