Yield Points
2022-11-02 · 1,063 chars · 6 min read
前端性能优化的关键之一,就是避免长任务(Long Task),尽可能的将长任务拆分为若干个小的任务,使得浏览器可以响应用户操作等高优先级的事情。以往任务的拆分,最简单粗暴的方式就是使用 setTimeout,比如:
fun1()
setTimeout(() => {
fun2()
}, 0)
这样可以确保 fun1 和 fun2 在两个独立的 Task 中(注意必须是 setTimeout 等 macrotasks,microtasks 不行)。这样虽然可以一定程度上解决问题,但是看着比较丑,逻辑也会被拆的很乱。
最近无意中看到一篇 web.dev 上的文章:Optimize long tasks,提到一个叫 yield points 的概念,算是对 setTimeout 的优雅升级,今天我们就一起来看看。
什么是 Yield Points#
yield points 可以理解为“让步点”,也就是在 yield points 的位置上,我们将主线程的控制权交还给浏览器,让浏览器可以优先响应用户的高优操作,比如点击事件等等。直接来看看具体的代码实现:
function yieldToMain() {
return new Promise((resolve) => {
setTimeout(resolve, 0)
})
}
这就是所谓的 yield points 了。上文也提到过,只有 macrotask 才可以打断长任务,所以 yield points 的核心依然是 setTimeout,而 Promise 只是为了让我们的代码更加优雅,方便使用 async/await 来组织。
我们看个具体的实例:
function fun() {
let total = 0
for (let i = 0; i < 20000000; i++) {
total = total + i
}
console.log(total)
}
async function run() {
const fun1 = () => fun()
const fun2 = () => fun()
const fun3 = () => fun()
const fun4 = () => fun()
const fun5 = () => fun()
const tasks = [fun1, fun2, fun3, fun4, fun5]
while (tasks.length > 0) {
const task = tasks.shift()
task()
}
}
run()
fun 就是模拟一个比较耗时的任务,我们主要执行的是 run 函数,它里面会依次执行 5 个耗时较长的任务。火焰图如下:
可以清楚的看到,fun1 到 fun5 全部集中在一个 120ms+ 的长任务里。作为对比,我们加上 yieldToMain 再看看。
//...
while (tasks.length > 0) {
const task = tasks.shift()
task()
// 只添加了这一行 !!!
await yieldToMain()
}
//...
执行并查看火焰图:
在任务调度的时候,每个任务执行完,都执行一次 yieldToMain,拆开长任务,让步于主线程。另外,对比两张图可以发现,页面的 DOMContentLoaded 和 Onload 两个事件都提前了。
文章到这里,基本就说明白什么是 yield points 了。但是,现在是在每次子任务执行后,手动调用 yieldToMain,如果每个子任务都执行的很快,那这么做显然是没必要的。我们需要一种方法,仅在真正有必要的时候执行。
按需执行 Yield Points#
isInputPending() 函数可以用来判断用户是否正在和页面发生交互,是的话返回 true,否则 false。我们可以方便的使用这个 API 来判断是否需要让步于主线程。不过目前兼容性还不太好,可以通过 js 的运行时间做个兜底。
// 长任务是大于 50ms 的
let deadline = performance.now() + 50
while (tasks.length > 0) {
if (navigator.scheduling?.isInputPending() || performance.now() >= deadline) {
await yieldToMain()
// 延长 deadline
deadline += 50
continue
}
const task = tasks.shift()
task()
}
总结#
简单梳理下目前打断长任务的方案:
- 简单的场景,直接
setTimeout即可。 - 相对复杂一些,涉及到较多任务的,可以使用比较优雅的
yieldToMain。 - 更复杂的情况,无法直观判断 yield points,可以利用
isInputPending+ 时间,实现一定程度上的自动调度。
这些方案的核心还是 setTimeout,插到任务循环的队尾,无法控制优先级。chrome 实现了一个新的 API:postTask,可以实现任务的优先级调度,目前兼容性还不太好。不过相信等这些 API 普及之后,前端在任务处理方面会更加的精细,面对复杂需求时依然能给用户提供良好的体验。




