关于优化浏览器的长任务
为了能让 Javascript 应用能够保持快速运行,我们通常要求:
- 不要阻塞主线程
- 拆分长任务
任务
任务是指浏览器执行的任何离散工作。这些工作包括渲染、解析 HTML 和 CSS、运行 JavaScript,以及您可能无法直接控制的其他类型的工作。在所有这些因素中,而编写的 JavaScript 可能是任务的最大来源。
主线程
主线程是浏览器中运行大多数任务的线程,也是执行您编写的几乎所有 JavaScript 的线程。
主线程一次只能处理一项任务。任何耗时超过 50 毫秒的任务都是长任务。对于超过 50 毫秒的任务,任务的总时间减去 50 毫秒即为任务的阻塞时间。
当任何长度的任务运行时,浏览器都会阻止互动发生,但只要任务运行时间不太长,用户就不会察觉到这一点。不过,当用户尝试与包含许多长时间任务的网页互动时,用户界面会感觉无响应,如果主线程被长时间阻塞,甚至可能会出现故障。
于是,我们可以利用setTimeout(func,0)来将长任务拆分,加入到事件循环的下一个周期,也就是事件循环中的宏任务队列中,由于setTimeout是异步的,这样就不会阻塞主线程了。
对于setTimeout,即使指定了0的超时时间,此方法也会将回调的执行推迟到单独的任务中。
从:
function saveSettings () {
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
到:
function saveSettings () {
// Do critical work that is user-visible:
validateForm();
showSpinner();
updateUI();
// Defer work that isn't user-visible to a separate task:
setTimeout(() => {
saveToDatabase();
sendAnalytics();
}, 0);
}
这称为“让步”,最适合需要按顺序运行的一系列函数。
但是它也有一些缺点:
- 由于
setTimeout的回调被推迟到事件循环的下一个周期,会被加入到宏任务队列的末尾,这可能会导致后面的任务先于应该先执行的任务执行。 - 如果是在循环中:
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
在此处使用 setTimeout() 会出现问题,并且在嵌套了五轮 setTimeout() 后,浏览器将开始为每个额外的 setTimeout() 强制实施至少 5 毫秒的延迟。
于是乎要引入一个专用的浏览器的程序让步API:
scheduler.yield()
这个api属于浏览器的任务调度api,专门用来在浏览器中让出主线程。
它不是语言级语法或特殊构造;scheduler.yield() 只是一个返回 Promise 的函数,该 Promise 将在未来的任务中解析。链接到在 Promise 解析后运行的任何代码(无论是在显式 .then() 链中还是在异步函数中 await 后),都将在该未来的任务中运行。
在实践中:插入 await scheduler.yield(),函数将在该点暂停执行并让出主线程。该函数的其余部分(称为函数的延续)的执行将安排在新的事件循环任务中运行。当该任务开始时,被等待的 promise 将得到解析,并且函数将从上次停止的位置继续执行
async function saveSettings () {
// Do critical work that is user-visible:
validateForm();
showSpinner();
updateUI();
// Yield to the main thread:
await scheduler.yield()
// Work that isn't user-visible, continued in a separate task:
saveToDatabase();
sendAnalytics();
}
函数 saveSettings() 的执行现在分为两个任务。这样一来,布局和绘制就可以在任务之间运行,从而为用户提供更快的视觉响应(如现在短得多的指针互动所衡量的那样)。
使用 scheduler.yield() 时,延续会从中断的地方继续执行,然后再继续执行其他任务。
不是所有的浏览器都支持这个api,需要回退操作:
function yieldToMain () {
if (globalThis.scheduler?.yield) {
return scheduler.yield();
}
// Fall back to yielding with setTimeout.
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
我们还可以利用requestIdleCallback()来让出主线程,requestIdleCallback() 允许您安排一个回调函数在浏览器空闲时运行。与 scheduler.yield() 不同,requestIdleCallback() 不保证回调函数会在特定时间运行;相反,它会在浏览器认为合适的时候运行回调函数。
不仅如此,还有一个requestAnimationFrame(),它允许您安排一个回调函数在浏览器下次重绘之前运行。与 scheduler.yield() 不同,requestAnimationFrame() 旨在用于动画,并且回调函数将在浏览器准备好进行下一次重绘时运行。虽然本意是用于动画,但是它也可以用来让出主线程,尤其是在需要在用户界面更新之前执行一些工作时。
友链 👉 如何优化长任务?