JavaScript性能优化:(六)加快页面响应速度

大多数现代浏览器都是让一个线程共用于执行 JavaScript 代码和更新页面,也就是说,每一时刻浏览器只能执行其中一种操作,这意味着当 JavaScript 代码正在执行时页面就会无法响应,反之亦然。

浏览器 UI 线程

用于执行 JavaScript 和更新页面的进程通常被称为“浏览器 UI 线程”(尽管对所有浏览器来说,称为“线程”可能不准确)。、

UI线程的工作基于一个简单的队列系统,任务会被保存到队列中直到进程空闲。一旦空闲,队列中的下一个任务就被重新提取出来并运行。这些任务要么是待执行的 JavaScript 代码,要么是待执行的 UI 更新,包括重绘和回流。

当所有 UI 线程任务都执行完毕,进程进入空闲状态,并等待更多任务加入队列。空闲状态是理想的,因为用户所有的交互都会立刻触发 UI 更新。如果用户试图在任务运行期间于页面交互,不仅没有即时的 UI 更新,甚至可能新的 UI 更新任务都不会创建并加入队列。事实上,大多数浏览器在 JavaScript 运行时都会停止把新任务加入到 UI 线程的队列中,也就是说 JavaScript 任务必须尽快结束,以避免对用途体验造成不良影响。

浏览器限制

一般来说,浏览器会限制 JavaScript 任务的运行时间。

有两种方法可以度量脚本运行了多长时间。第一种是记录脚本开始以来执行的语句的数量。这种方法意味着脚本在不同的机器上可能会有不同的运行时间,因为可用内存和 CPU 速度会影响单个语句的执行时间。第二种方法是记录脚本执行的总时长,在指定时间内可运行的脚本数量也因用户的机器性能而有所差异,但是到达执行时间后,脚本会停止运行。毫无疑问的是,不同浏览器检测长时间运行脚本的方法会略有不同。

多久才算“久”

Nielsen 指出如果界面在 100 毫秒内响应用户输入,用户会任务自己在“直接操纵界面中的对象”。超过 100 毫秒意味着用户会感到自己与界面失去联系。由于 JavaScript 运行时无法更新 UJI,所以如果 JavaScript 运行时间超过 100 毫秒,用户就会感觉失去了对界面的控制。

最佳实践是限制所有的 JavaScript 任务在 100 毫秒或更短的时间内完成,以避免类似情况出现。

使用定时器

理解定时器

定时器并不是 ECMAScript 提供的 API,而是由宿主环境浏览器提供的。有一点需要注意的是,当定时器启动之后,只是将其绑定的任务加入到当前线程队列中进行排队,并不会马上执行它。

使用定时器处理数组

典型的数组循环模式如下:

1
2
3
for (let i = 0, len = arr.length; i < len; i++) {
process(arr[i]);
}

这类循环运行时间过长的原因主要是 process() 的复杂度或 arr 的大小,或两者兼有。

对于这种情况,假如数组无需严格按照顺序处理,且整个处理过程也不要求同步进行,那么我们可以使用定时器来分解此任务。

一种使用定时器帮助处理大数组的方式如下:

1
2
3
4
5
6
7
8
9
10
11
let todo = arr.concat(); // 克隆原数组
setTimeout(function handler(callback) {
process(todo.shift());
if (todo.length) {
setTimeout(handler, 25);
} else {
callback();
}
}, 25);

每个定时器的真实延时时间在很大程度上取决于具体情况。普遍来讲,最好使用 25 毫秒,因为再小的延时,对大多数 UI 更新来说普遍不够用。——《高性能JavaScript》

使用定时分解任务

根据前面使用定时器帮助处理大型数组的经验,对于繁杂的众多一般性任务,我们也可以使用定时器将它们分解为原子任务再处理。

例如,如果要在同一时间段内执行大量函数,我们可以将这些函数放到数组中,然后遍历数组,每隔一定间隔执行下一个函数,这样就可以减轻 JavaScript 线程的压力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function multistep(steps, args, callback) {
// steps:待执行函数组成的数组
// args:Array 类型的实例包裹的传递给待执行函数的参数,
// callback:处理结束式执行的回调函数
let tasks = steps.concat();
setTimeout(function handler() {
let tasks = tasks.shift();
task.apply(null, args || []);
if (tasks.length) {
setTimeout(handler, 25);
} else {
callback();
}
});
}

定时器性能

同一时间只有一个定时器存在,只有当这个定时器结束时才创建一个新的,通过这种方法使用定时器不会导致性能问题。当多个重发的定时器同时创建往往会出现性能问题。因为只有一个 UI 线程,而所有的定时器都在抢占运行时间。

间隔在 1 秒或 1 秒以上的低频率重复定时器几乎不会影响 Web 应用的响应速度。

当多个重复定时器使用较高的频率(100 ~ 200ms)时,Web 应用就会明显变慢,响应也不及时。

总结:在 Web 应用中限制高频率重复定时器的数量。建议是创建一个独立的重复定时器,每次执行多个操作。

Web Worker

本部分参见 Web Worker


参考

  • 《高性能JavaSript》,[美]Nicbolas C.Zakas 著,丁琛 译,电子工业出版社 2015