Contents

性能优化之分片

痛点

在进行耗时长的任务时,如果同步执行,会造成页面卡顿。比如js进行复杂的运算,页面渲染大量的DOM节点。在这种情况可以通过分片的思想来优化。例如React的Fiber就使用了这种思想来优先保证页面的操作。

概念

分片简单来说就是将一个复杂的同步任务,合理的拆分为很多个小任务,由调度器控制进行异步执行,保证当前帧只执行不会造成阻塞的任务,剩余的任务等到下一帧再执行。

在浏览器中,只要保证每个异步任务的执行时间不能超过 16ms,超过就停止执行,将控制权交给浏览器,等待下一个异步任务的执行。

注意

因为js是单线程(除非使用webworker),所以分片计算并不会加快计算速度,只是为了避免页面卡顿。

实现

普通的同步任务

console.time();
for (let i = 0; i < 10000; i++) {
  for (let j = 0; j < 1000000; j++) { }
}
console.timeEnd();

正常耗时要要2s左右,这期间页面会卡死,不会有任何响应。

异步分片计算

利用setTimeout实现。

const DEFAULT_RUNTIME = 16;
let sum = 0;
const runner = (tasks) => {
  const prevTime = performance.now();
  do {
    if (tasks.length === 0) {
      return;
    }
    const task = tasks.shift();
    const value = task();
    sum += value;
  } while (performance.now() - prevTime < DEFAULT_RUNTIME);

  setTimeout(() => runner(tasks));
};
const tasks = [];
for (let i = 0; i < 10000; i++) {
  tasks.push(() => {
    for (let j = 0; j < 1000000; j++) { }
  });
}
// 这里先只看 runner 的耗时
console.time();
runner(tasks);
console.timeEnd();

但这样的调度任务存在一些问题:

  1. setTimeout 的最小值是 4ms,造成了时间的浪费,考虑到一帧 16ms,4ms 是一个很大的开销。。

  2. 调用方无法知道什么时候调用结束了。

  3. 调用方无法手动取消任务调用。

根据上面问题,对调度器进行一些优化:

  1. setTimeout 换成MessageChannel

那么为什么要使用 MessageChannel,而不是 requestAnimationFrame 呢?raf 的调用时机是在渲染之前,但这个时机不稳定,导致 raf 调用也不稳定,所以不适合。

MessageChannel 也是 React 调度使用的方案,如果浏览器不支持,才会降级到 setTimeout

  1. 可以利用 Promise 的特性来进行封装。
    1. 在执行成功时执行 Promiseresolve 方法。
    2. 在执行失败时执行promisereject方法。
    3. 设置一个标志位,如果标志位是 false,就取消后续调用。对外暴露一个abort方法,来修改标志位,取消调用。
const scheduler = (tasks) => {
  const DEFAULT_RUNTIME = 16;
  let sum = 0;
  let isAbort = false
  const { port1, port2 } = new MessageChannel();
  const promise = new Promise((resolve, reject) => {
    const runner = () => {
      const prevTime = performance.now();
      do {
        if (isAbort) {
          return reject()
        }
        if (tasks.length === 0) {
          return resolve(sum)
        }
        const task = tasks.shift();
        try {
          const value = task();
          sum += value;
        } catch (e) {
          reject(e)
        }
      } while (performance.now() - prevTime < DEFAULT_RUNTIME);
      port2.postMessage('')
    };
    port1.onmessage = function () {
      runner();
    };
    port2.postMessage('');
  })
  promise.abort = () => {
    isAbort = true
  }
  return promise
}
console.time();
const tasks = [];
for (let i = 0; i < 10000; i++) {
  tasks.push(() => {
    for (let j = 0; j < 1000000; j++) {
    }
    return i
  });
}
scheduler(tasks)
console.timeEnd();

完整TypeScript 实现

interface Scheduler extends Promise<void> {
    abort?: () => void;
}
export const scheduler = (tasks: Function[]) => {
    const DEFAULT_RUNTIME = 16;
    let isAbort = false
    const { port1, port2 } = new MessageChannel();

    const promise: Scheduler = new Promise((resolve, reject) => {
        const runner = () => {
            const prevTime = performance.now();
            do {
                if (isAbort) {
                    return reject(new Error('abort'))
                }
                if (tasks.length === 0) {
                    return resolve()
                }
                tasks.shift()()
            } while (performance.now() - prevTime < DEFAULT_RUNTIME);
            port2.postMessage('')
        };
        port1.onmessage = function () {
            runner();
        };
        port2.postMessage('');
    })
    promise.abort = () => {
        isAbort = true
    }
    return promise
}