时间分片(Time Slicing)
Opened this issue · 0 comments
时间分片
W3C性能工作组规定:将执行时间超过50ms任务定义为长任务(Long Task)。
长任务由于长时间阻塞主线程,会让用户感觉到卡顿。
而解决长任务的方式大致有两种:
- 使用Web Worker,将长任务放在 Worker 线程中执行,缺点是无法访问 window 对象和 操作 DOM
- 时间切片(Time Slicing)
什么是时间分片
时间分片并不是某个 api,而是一种技术方案,它可以把长任务分割成若干个小任务执行,并在执行小任务的间隔中把主线程的控制权让出来,这样就不会导致UI卡顿。
React 的 Fiber 技术核心**也是时间分片,Vue 2.x 也用了时间分片,只不过是以组件为单位来实施分片操作,由于收益不高 Vue 3 把时间分片移除了。
使用时间分片
在早期,时间分片充分利用了“异步”来实现,例如:
btn.onclick = function (){
someTask(); //50ms
setTimeout(function() {
otherTask(); //50ms
})
}上面代码,本来应该执行 100ms 的长任务,被拆分成了两个 50ms 的任务。
使用 Generator 函数
Generator是 ES6 里的语法,它提供了一个生成器函数来生成迭代器对象,我们利用 Generator 函数提供的 yield 关键字来让函数暂停,通过使用迭代器对象的 next 方法让函数继续执行。
如果我们用 Generator 函数,则可以这么写:
btn.onclick = ts(function* (){
someTask();
yield;
otherTask();
})这样就可以通过 yield 把一个长任务拆分成两个短任务。
我们也可以将 yield 关键字放在循环里:
btn.onclick = ts(function* (){
while (true) {
someTask();
yield;
}
})上面虽然是个死循环,但依然不会阻塞主线程,所以浏览器不会卡死。
基于 Generator 函数实现 ts 方法
基于 Generator 函数的执行特性,我们很容易使用它来实现一个时间分片函数:
function ts(gen) {
if (typeof gen === 'function') gen = gen();
if (!gen || typeof gen.next !== 'function') return;
return function next() {
const res = gen.next();
if (res.done) return;
setTimeout(next);
};
}代码核心**:通过 yield 关键字可以将任务暂停执行,并让出主线程的控制权;通过setTimeout将未完成的任务重新放在任务队列中执行。
演示
为了好理解,先写段长任务代码,将主线程阻塞一段时间:
const start = performance.now();
let count = 0;
while (performance.now() - start < 1000) {}
console.log('done!');该段脚本霸占主线产长达 1s 的时间,如果把这段长任务分解成多个小任务执行呢。
我们通过这种方式来看一下,将长任务使用时间分片来处理:
ts(function* (){
const start = performance.now();
let count = 0;
while (performance.now() - start < 1000) {
yield;
}
console.log('done!');
})()从图里看到,一个长任务虽然切成了诺干个小任务,但时间颗粒度过小,这样会导致执行任务的总时长增加,而W3C定义超过 50ms 为长任务,所以我们要控制一下任务时长,让它在一个合理的时间内,这样不会导致任务总时长过长。
继续优化
为了保证切割的任务接近 50ms,可以在 ts 函数中根据任务的指向时间判断是否应该一次性执行多个任务。
修改一下 ts 函数:
function ts(gen) {
if (typeof gen === 'function') gen = gen();
if (!gen || typeof gen.next !== 'function') return;
return function next() {
const start = performance.now();
const res = null;
do {
res = gen.next();
} while (!res.done && performance.now() - start < 25);
if (res.done) return;
setTimeout(next);
};
}上面代码中,做了一个 do while:如果当前任务执行时间低于 25ms 则多个任务一起执行,否则作为一个任务执行,一直到 res.done = true 为止。
可以看到,时间切片的颗粒度变的正常了,总时间也会相应缩短,完美!
总结
时间分片的概念以及技术方案让长任务分割成多个短任务,并且将控制权放给主线程,不会造成主线程卡顿
通过使用 Generator 函数特性,很方便的实现了 ts 函数。


