在前端开发中,长时间运行的JavaScript任务一直是一个棘手的问题。它们会导致页面无响应,影响用户体验。传统上,开发者使用setTimeout()
来分割长任务,但这种方法存在明显的缺陷。最近,Chrome 129引入了一种新的、更高效的方法:scheduler.yield()
。本文将深入探讨这种新技术,并比较其与传统方法的优劣。
长任务的问题
为了说明长任务的问题,以下是一个示例,任何字符都有其代码,但并非所有代码都有相关字符。所有非字符代码都显示为白色垂直矩形。您可以在下面显示与一系列代码相对应的字符的页面中看到许多代码:
有很多垂直的矩形,想把它们过滤掉,通过遍历一系列Unicode字符代码,过滤掉未分配的代码点。
const MIN = 127734, MAX = 129686;
function insertChar(code, parent) {
parent.insertAdjacentText('beforeend', String.fromCodePoint(code));
}
function add(i, parent) {
if (!likeNull(i)) // 比较字符i和0的canvas
insertChar(i, parent);
}
function one(div) {
for (let i = MIN; i < MAX; i++)
add(i, div);
}
function onClick(func) {
btn.addEventListener('click', async () => {
btn.remove();
const start = Date.now();
await func(div);
div.insertAdjacentText("afterend", Date.now() - start);
});
}
onClick(one);
这段代码在执行过程中会使页面冻结约4279毫秒,而在此期间,页面一直处于冻结状态。即使点击后代码会首先移除按钮,但只要代码还在运行,浏览器就无法更新屏幕。代码运行结束后,浏览器也无法显示任何字符,按钮会被移除,字符也会一并显示, 导致用户体验不佳。
因此,在长时间工作时,一定要暂停,让浏览器更新屏幕。可以使用类似的语句暂时中止或中断长代码的执行.
使用setTimeout()分割任务
传统的解决方法是使用setTimeout()
来分割任务:
function pause() {
return new Promise(resolve => setTimeout(resolve));
}
async function two(div) {
for (let i = MIN; i < MAX; i++) {
await pause();
add(i, div);
}
}
onClick(two);
这种方法虽然使页面保持响应,但执行时间显著增加到约17568毫秒。
setTimeout()的缺点
1.最小超时时间为4毫秒,即使指定为0
即使浏览器无事可做,主任务也会暂停至少 4 毫秒。即使指定为零,setTimeout()的最小超时时间 也是 >4 ms。事实上,让我们来计算一下。在第一页中,评估 1952 个代码点需要 4279 毫秒,即每个代码需要 ~2 毫秒。在第二个页面中,评估 1952 个代码点需要 17568 毫秒,即每个代码点 ~17568/1952=9毫秒。页面响应速度保持不变,但性能下降也令人印象深刻,这主要是由于超时的持续时间尽可能短。
2.任务继续执行时被放置在队列末尾,可能导致优先级问题。
当任务暂停时,setTimeout()
会将其作为一个新任务放置在队列的最末端。因此,浏览器不仅会更新屏幕,还会先执行队列中的所有任务,然后再继续执行暂停的任务。
在上面的页面中,两个函数two()同时运行:
onClick(()=>Promise.race([two(div),two(div2)]));
当第一个two()提交给主线程时,第二个two()会在第一个two()继续执行之前被执行。执行时间会稍有增加,从 17 秒增加到 23 秒,但不会增加两倍,因为大部分运行时间都是由最小超时加起来的。
scheduler.yield():新的解决方案
scheduler.yield()
提供了一种更高效的方法来让出主线程:
async function three(div) {
for (let i = MIN; i < MAX; i++) {
await scheduler.yield();
add(i, div);
}
}
onClick(three);
使用scheduler.yield()
,执行时间减少到约5646毫秒,显著优于setTimeout()
方法。
scheduler.yield()的优势
更高的性能:执行时间更接近于未中断的任务。 优先级处理:被暂停的任务被放置在队列头部,而不是末尾。
示例比较:
// 同时执行两个three()函数
onClick(() => Promise.race([three(div), three(div2)]));
// three()与setTimeout()基于的two()比较
onClick(() => Promise.race([three(div), two(div2)]));
结果与上述基于setTimeout()的index4.html的不同之处仅在于执行时间。10183 毫秒相当于两次 5645 毫秒。两个任务似乎都没有优先级。
优先级似乎不起作用,因为两个函数three()都不在队列中等待。但看看这个:
在与setTimeout()
基于的方法比较时,scheduler.yield()
显示出明显的优先级优势。
结语
scheduler.yield()
为JavaScript中的任务调度提供了一个强大的新工具。它不仅性能更优,还能更好地处理任务优先级。对于需要处理长时间运行任务的前端开发者来说,这是一个值得关注和采用的新技术。
在实际应用中,开发者可以考虑在处理大量数据处理、复杂计算或频繁DOM操作等场景时使用scheduler.yield()
。这将有助于保持页面的响应性,同时不会显著影响任务的执行效率。随着浏览器支持的增加,scheduler.yield()
有望成为前端性能优化的重要工具。