实战篇 - 如何实现和淘宝移动端一样的模块化加载 (task-silce)

20,451 阅读5分钟

事情背景

事情的背景是我的实际项目,在我的实际项目当中,发现首屏渲染的速度比较慢,这样导致白屏的时间会特别长,影响用户体验度,恰好我有一天看了淘宝移动端的加载方式,针对我们的项目,就做了一次优化调整,并写了一个简单的工具库

这个工具是不限制环境和框架的,我现在的框架是 vue ,在 react 及小程序中也做过测试,是完全可以使用的,使用建议大家仔细阅读这篇文章,希望可以对你的工作有所帮助

性能优化的目的

我们每一次的界面变化,都要经历以下步骤:

人的眼睛大约每秒可以看到 60 帧,那么就代表我们每 16.7ms 就要看到 1 帧,一帧就要经历上图的 5 步,说明我们的每一个任务(task) 不宜过长,这样就会导致用户对于界面感知的不友好性

根据谷歌统计的数据,用户在不同时间段内接收到的反馈,可能直接影响到对于网站的用户留存,如下图:

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

分析淘宝

淘宝的渲染方式

我们先看一下淘宝的渲染方式

通过图片和 Performancemain 部分,我们可以看得出来淘宝移动端的加载方式,是一块一块去加载的,暂时我们称之为 模块化加载

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