web资源预加载-生产环境实践

Posted 飞翔的熊blabla

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了web资源预加载-生产环境实践相关的知识,希望对你有一定的参考价值。

此文记录资源预加载在我们项目的实践,技术难度不算高,重在介绍一套技术方案的诞生与实施,其中都进行了哪些思考,依据什么来做决策,如何进行效果评估,等等。为读者在制定技术方案时提供一定启示。

背景

资源预加载机制很好理解,即在用户访问页面之前,提前加载好相应的资源。这样用户在访问页面的时候,省去了加载资源的时间,达到“秒开”的效果。

资源预加载的方案很多,本文所述的是纯web下的资源预加载,区别于利用容器做资源预置。所以采用的技术都是纯web方案。

另外还有一个背景:项目是SPA架构,webpack+vue全家桶,我们做的是在加载完首页之后的事情,即跳转其他页面时的预加载。配合SPA应用的优势,可以实现媲美原生应用的零延迟跳转。

下面来介绍下技术细节。

预加载哪些资源

可以预加载的资源是很多的,页面异步chunk、vue组件、js模块,甚至是接口数据也可以预请求。所以就要看你的目的是什么了。

此处我们以零延迟跳转页面为目标,所以要预加载的就是页面异步chunk。所谓页面异步chunk,是指使用vue-router定义的页面路由所对应的异步加载的文件,其实也是个vue组件,为了跟其他vue组件区分,我们管它叫页面异步chunk吧。代码里一般这么写的:

const routes = [
    name: 'home',
    path: '/home',
    children: [
        
            path: 'A',
            component: () => import('pageA.vue'),
        ,
        
            path: 'B',
            component: () => import('pageB.vue'),
        
    ]
]

pageA.vue和pageB.vue打包出的异步chunk在对应的路由下才会加载,给页面跳转带来延迟感,我们要预加载的就是这部分资源啦。

资源加载时机

明确了要加载的资源,接下来要考虑的是,什么时候加载这些资源呢?如果我们预加载了pageB而用户却不跳转B怎么办呢?

资源加载时机,这是个技术活,需要抓住两个关键点:

1.预加载资源应该尽量不影响用户的正常操作

2.预加载的资源要尽量保证用户能使用到

第1点,很容易想到,我们可以在页面空闲的时候去预加载。而且还要尽早,如果你加载晚了,用户就用不上你预加载的资源了。总结一下,资源加载时机就是“尽早的在页面空闲的时候”。

第2点,这就不好办了,用户跳不跳B页面是用户行为,我们怎么能保证他一定能用到预加载的资源呢。所以这个只能采取策略,保持一个可接受的比例,在“浪费流量”与“用户体验”之间找到权衡点。

下面就这两点展开说一说。

页面空闲检测

如何检测页面是否空闲呢?很不幸目前没有这样的api可用,有一个requestIdleFrameCallback,支持在每帧绘制空闲期执行一个回调函数,但不能确保一定执行。

所以我们只能自己想办法判断了,思路其实也比较简单,浏览器的渲染进程有js引擎和UI引擎这两个线程,只要这两个线程是空闲的,我们就认为页面是空闲的,这应该是行得通的。

那么,如何检测这两个线程空闲呢?其实很简单,看代码就明白了:

// 检测js线程空闲
const d1 = new Date();
setTimeout(()=> 
    const offset = new Date() - d1;
    if (offset < 25 )  
      // JS线程空闲 
    
), 20);

启动一个延时函数,查看真正执行时候的延时是多少,如果延时很大,那说明当前js引擎繁忙。如果小于某个阈值,那说明js引擎空闲。

这个阈值该如何确定呢?统计真实用户的情况肯定是最准确的,所以我们用一个空闲页面统计了用户的延时均值,最终确定为5ms。

同样的思路,UI引擎的空闲也可以检测出来:

// 检测UI线程空闲
const d1 = new Date();
requestAnimationFrame(()=>
    const offset = new Date() - d1;
    if ( offset < 30 )  
      // UI线程空闲 
    
)

阈值的确定也是统计的真实用户均值,最终确定为30ms。

有了这两项检测,我们就拿到了页面的“空闲时刻”。那如何“尽早的”拿到呢?做法相对简单,我们在页面加载完后用setInterval启动轮询,每隔1秒检测一次,一但检测到空闲,就进行资源预加载。

资源清单策略

接下来看第2点,如何在“浪费流量”与“用户体验”之间找到权衡。

考虑一个问题,如果只有5%的用户会从首页跳转页面B,那B的资源有必要预加载吗?答案显然是不需要。也就是说需要预加载的资源是要人工确定的,那个我们依据什么来确定资源清单呢?

有两条途径:

1.页面访问漏斗图。根据漏斗图我们能够得到每一次页面跳转的流失率,对于流失率较大的页面,我们就可以不去预加载了。多大的值算”较大“,这也是需要权衡的,比如我们认为60%就算流失较大了。一个典型的漏斗图如下:

2.根据统计指标动态调整。我们的资源预加载方案效果怎样,成本收益比怎样,是需要明确指标来衡量的。给指标造成负向作用的页面,就不去预加载它了。

那么,统计指标该如何设计呢?我们继续来讲。

指标设计

指标是为了用数据化的方式来评估和指导我们的工作、决策。既然是生产环境实践,就得有一套严谨的指标来衡量这个方案的优劣。

上文提到我们统计了用户执行setTimeout和requestAnimationFrame的延时均值,用于指导我们设定空闲检测阈值,就是一例应用。

对于整套方案,我们还设计了以下指标:

页面跳转时间

即用户跳转页面的耗时均值。这是我们要关注的核心指标,整个方案的目标就是降低页面跳转时间。由于是SPA应用,这个时间是比较容易统计到的。

触发率

即触发了资源预加载的比例,计算公式:触发率 = 资源预加载次数 / 页面加载次数。用来衡量我们的加载策略(空闲检测)是不是合理,理想情况下这个值应该接近100%,也就是说绝大多数的用户都预加载到了资源。如果发现触发率不及预期,那我们应该去调整加载策略。

命中率

即用户使用了预加载资源的比例,计算公式:使用了预加载的资源次数 / 资源预加载次数。这是用来衡量预加载资源的有效性的,比如一个资源的命中率是80%,说明80%的用户都使用到了预加载的资源,效果是非常好的。但实际的命中率往往达不到这么高,所以我们评估的标准是:与漏斗图的比例越接近,说明效果越好。

页面停留时间

即用户在一个页面的平均停留时间。这个指标也是为了指导加载策略而采集的,比如首页的平均停留时间是3秒,那么我们检查页面空闲的轮询间隔可以设为600ms一次,共检测5次。

除此之外,我们还采集了用户的网速情况,用以分析这个方案对不同网速段的用户的影响情况。

有了以上指标,我们就能够科学的制定策略了,比如某个资源的命中率低于10%,那说明是个低频页面,干脆从清单中剔除掉,不预加载了。

如何加载资源

整个思路已经清晰了起来,接下来到了加载资源环节。话说预加载资源的方式有很多种,我们选哪种呢?

不妨一一来看。

1.手写script标签

项目是用webpack打包的,通过manifest文件可以拿到资源清单,在需要预加载的时候手动创建一个script标签,用户真正跳转的时候就可以使用缓存中的资源。

这个方法的优点是侵入性较小,只是多了一次额外的资源加载,对原有代码改动很小。

但缺点也很明显:不好统计指标。要统计加载完成可以在每个script标签的onload事件里,还可以接受。但是统计命中率就没办法了,没法判断用户使用的是缓存中的资源还是新加载的。

2.浏览器prefetch

即使用<link rel="prefetch" href="xx.js">,这是浏览器提供的预加载资源方式,它会在浏览器空闲的时候自动给加载资源。

这个方式能力有点弱,因为没有js API供调用,对我们是黑盒的,没法进行相关的指标统计。

3.webpack提供的import()

webpack提供的动态加载资源的方法,虽然本质上也是写script标签,但webpack给做了很好的封装,我们通过.then()就可以知道资源加载完毕。而且资源被保存在内存中,一方面便于统计各种指标,另一方面连缓存都不必走了,速度更快。

综上,我们最终选择了方法3,能够满足我们的各项需求。

效果评估

以上就是整个方案的技术内容了,但事情到这里还不能结束。跟踪评估方案的效果,并持续优化,这才是生产环境实践的正确姿势。

我们关心的核心指标,页面跳转时间,有了50%以上的降低。这是预期之内的,之前页面跳转比如耗时200ms,命中预加载的资源后,可能一下就到了10ms以内了。这对大盘的影响是很显著的。

触发率这个指标,应该至少得在90%以上才算合格。都没触发预加载,后续还怎么谈命中。当然这个指标是可以通过调整加载策略来优化的,后面会讲。

至于命中率,就不太好说了。首先是上文提过的,它与用户行为相关,是个不稳定因素。其次,如果资源预加载触发的太晚,用户已经自己去跳转了,也会影响这个指标。所以整体从15%~35%不等吧。这个值是什么水平呢?合格还是不合格?我们通过翻阅业界也做预加载方案的资料,看到他们得到的命中率也大概在20%,所以可以认为我们的效果也算不错了。

持续优化

大家也看到了,我们的指标是存在优化空间的,方向就是:提高触发率、命中率,降低页面跳转时间。

上面也有谈到,我们为了制定出最精确的加载策略,采集了很多辅助指标。一个有意思的优化是,我们之前是进入页面1秒后开始检测空闲的,后来改成了立即进行检测。没想到这个改动给触发率带来了显著提升,原因是有部分用户在1秒内就跳到其他页面了,根本来不及触发预加载。

类似的优化还有,我们调整空闲检测的时间间隔,发现对触发率也有影响。

另外,我们也根据统计到的命中率,剔除了一些低频页面,避免这些资源拉低命中率。

总而言之,各项指标的参考值就是一个权衡的过程,需要根据自己的业务情况来商定。

更多思考

作为一个性能优化的方案,我们只是在特定项目架构、特定场景下做了一部分探索。事实上这个课题能做的还远不止此。

就比如资源加载策略,难道只能在页面空闲的时候加载吗?在PC时代,有些方案是根据鼠标的轨迹来猜测用户的下一步动作,进而决定预加载哪些资源。在移动端,我们是否可以根据滑动动作来做预测,又或者,是否可以把用户行为统计(业务埋点)作为参照,这都是可以探索的方向。

有朋友可能会问,SPA应用把首页不需要的资源给按需加载,结果你还用预加载又给加回来了,这不是多此一举吗?干脆打包成一个文件不就行了?听起来好像很有道理,不过细细思考一下,打包成一个文件和SPA+预加载,这两种的优劣各拉出清单对比一下,就知道还是有区别的。这就需要思辨能力了。

所以本文的目的就在于,描述一个技术方案的设计与落地过程,给大家提供技术上或是方法论上的参考。

以上是关于web资源预加载-生产环境实践的主要内容,如果未能解决你的问题,请参考以下文章

用户行为分析模型实践—— 漏斗分析模型

考虑 application.properties 的预加载数据的 Spring 数据最佳实践

01 - nginx - 安装

通过link的preload进行内容预加载

通过link的preload进行内容预加载

前端性能优化 – 资源预加载