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 普及之后,前端在任务处理方面会更加的精细,面对复杂需求时依然能给用户提供良好的体验。