事情背景
事情的背景是我的实际项目,在我的实际项目当中,发现首屏渲染的速度比较慢,这样导致白屏的时间会特别长,影响用户体验度,恰好我有一天看了淘宝移动端的加载方式,针对我们的项目,就做了一次优化调整,并写了一个简单的工具库
这个工具是不限制环境和框架的,我现在的框架是 vue ,在 react 及小程序中也做过测试,是完全可以使用的,使用建议大家仔细阅读这篇文章,希望可以对你的工作有所帮助
性能优化的目的
我们每一次的界面变化,都要经历以下步骤:

人的眼睛大约每秒可以看到 60 帧,那么就代表我们每 16.7ms 就要看到 1 帧,一帧就要经历上图的 5 步,说明我们的每一个任务(task) 不宜过长,这样就会导致用户对于界面感知的不友好性
根据谷歌统计的数据,用户在不同时间段内接收到的反馈,可能直接影响到对于网站的用户留存,如下图:

在这里我们不深入讲对于这方面的一些细节,这篇文章主要是给大家讲一下,如果做任务切片,如何优化界面的渲染速度和响应速度
分析淘宝
淘宝的渲染方式
我们先看一下淘宝的渲染方式

通过图片和 Performance
的 main
部分,我们可以看得出来淘宝移动端的加载方式,是一块一块去加载的,暂时我们称之为 模块化加载
performance
的使用和如何查看性能优化的数据,可通过 性能优化篇 - Performance(工具 & api) 来了解 performance
淘宝的任务切片

我们放大以后可以看的出来,淘宝网在每一次的任务完成后,都会进行上面的 5 步进行界面的渲染,这样可能不如把所有的界面全部渲染完毕后,在进行样式计算、布局、绘制、计算位置等的速度快,但是这样可以保证,让用户在最短的时间内,可以看到我们的网站内容
简单的介绍一下渲染的步骤和对用户的影响,及淘宝的渲染方式,接下来我们开始实现一个任务切片的工具
任务切片源码介绍
任务切片,顾名思义就是我们要把每一个任务去做切片,缩短任务的执行时长,加快任务的渲染
这里要使用 es6 的 generator
的特性去实现任务切片
初始化任务
function init({ sliceList, callback }) {
if (!isFunction(callback)) {
console.error('callback 为必传参数并为 function');
return;
}
// 添加切片队列
this.generator = this.sliceQueue({
sliceList,
callback
});
// 开始切片
this.next();
}
在一开始的时候,我们需要至少两个参数:
sliceList
或者 sliceCount
: 可以是数组,也可以是数字,数组就是用来切对应的内容去分块,数字就是按次去切片
callback
: 这里需要使用者传一个回调函数,用来通知使用者切片到什么位置
切片队列
function* sliceQueue({ sliceList, callback }) {
let listOrNum = (isNum(sliceList) && sliceList) || (isArray(sliceList) && sliceList.length);
for (let i = 0; i < listOrNum; ++i) {
const start = performance.now();
callback(i);
while (performance.now() - start < 16.7) {
yield;
}
}
}
由于可以接收数组和数字,所以要先做兼容处理
接下来就是核心代码其中之一了:
我们要记录回调的执行时间,如果执行需要的时间少于 16.7ms,就停止继续执行下去,释放主线程让主线程可以利用这个时间再去做别的事情
如果大于的话,就在下一次绘制的时候去执行
这个时候大家可能会比较好奇,我们为什么要对任务执行时间短的去做切片,时间长的就不切呢?
其实这个要结合下一段代码来看,大家就会了解的比较清楚了
何时执行下一个切片任务
function next() {
const { generator } = this;
const start = performance.now();
let res = null;
do {
res = generator.next();
}
while (!res.done && performance.now() - start < 16.7);
if (res.done) return;
raf(this.next.bind(this));
}
有了这段代码,上面最后的长任务的执行没有打断就很好理解了
还是一样,任务执行的时间少于 16.7ms 就继续执行下一个切片任务
如果要是大于的话,我们就不需要执行下一个切片了,我们就要在下一次绘制(requestAnimFrame)的时候,去执行该任务,这样就可以把每一个任务给切开了
使用方法
npm install task-slice
TaskSlice.init(number || array, function(i){
//i 执行到第几次,或者第几个切片任务
})
到这里,我们就可以模仿像淘宝一样的模块化的方式去加载,下图是我自己使用该工具库做的优化前后的数据统计:


很明显,我们的对于用户的响应速度和界面渲染速度,提升了 50% 左右。
后续
git 地址:github.com/nextdoorUnc…
目前已发布 1.0.0 版本,下一版本可能会支持 promise 或者 控制切片时间,这个看具体的需求,及大家的反馈,我会定期进行对该工具库的更新
该工具已经在 npm 发了包,也在 git 提交了项目,有兴趣的可以去看看,顺便点个 star ,谢谢了。
结尾
已经有 n 久没有写过文章了,由于最近工作比较忙,而且项目当中对于前端性能还有架构方面的挑战性还是比较多的,这次是在做性能优化的时候,做的总结,接下来我会尽量多分享这种用于实际项目当中的优化方案,感谢大家的支持,谢谢。
还要感谢一下 berwin,是他提出的时间切片给了我灵感,这是他的 git 地址:github.com/berwin