chenlong-io/blog

时间分片(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 函数。