性能优化之分片
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();
但这样的调度任务存在一些问题:
-
setTimeout
的最小值是 4ms,造成了时间的浪费,考虑到一帧 16ms,4ms 是一个很大的开销。。 -
调用方无法知道什么时候调用结束了。
-
调用方无法手动取消任务调用。
根据上面问题,对调度器进行一些优化:
- 将
setTimeout
换成MessageChannel
。
那么为什么要使用
MessageChannel
,而不是requestAnimationFrame
呢?raf
的调用时机是在渲染之前,但这个时机不稳定,导致raf
调用也不稳定,所以不适合。
MessageChannel
也是 React 调度使用的方案,如果浏览器不支持,才会降级到setTimeout
。
- 可以利用
Promise
的特性来进行封装。- 在执行成功时执行
Promise
的resolve
方法。 - 在执行失败时执行
promise
的reject
方法。 - 设置一个标志位,如果标志位是 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
}