此文记录资源预加载在我们项目的实践,技术难度不算高,重在介绍一套技术方案的诞生与实施,其中都进行了哪些思考,依据什么来做决策,如何进行效果评估,等等。为读者在制定技术方案时提供一定启示。
背景
资源预加载机制很好理解,即在用户访问页面之前,提前加载好相应的资源。这样用户在访问页面的时候,省去了加载资源的时间,达到“秒开”的效果。
资源预加载的方案很多,本文所述的是纯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+预加载,这两种的优劣各拉出清单对比一下,就知道还是有区别的。这就需要思辨能力了。
所以本文的目的就在于,描述一个技术方案的设计与落地过程,给大家提供技术上或是方法论上的参考。