测试前两天提给我一个bug,点击一个按钮后页面会卡顿很久,然后才能出现弹窗。
我自己试了一下,点击按钮后,果然整个页面陷入了完全的卡顿,过了一会后弹窗出现,但此时操作页面同样会卡顿,一看就是js执行阻塞了渲染。
我又点击了其他数据同样位置的按钮,此时却一点都不卡顿。
可以推测应该是数据量太大导致处理函数的执行时间过长阻塞了渲染。
数据处理优化
于是找到处理函数,发现其中对列表数据进行了循环,又在循环中在一个大的字典表做字典匹配
ini
// 示例代码
async function buildOptions(list) {
for (let i = 0; i < list.length; i++) {
const dictList = await getDict(config.dicType);
}
}
async getDict(dicType) {
let list: DicItem[] = [];
if (!this.dictionaryList.length) {
list = await this.getDictAll();
} else {
list = this.dictionaryList;
}
return list.filter(o => o.id == dicType);
}
由于是大数据的循环套循环,所以只要将字典表做一次循环变为map,就可以让效率提升。
kotlin
async getDict(dicType) {
if (!this.dictionaryList.length) {
await this.getDictAll();
}
return this.dictionaryMap.get(dicType);
}
改好,再次点击按钮,熟悉卡顿、熟悉的掉帧,完全没有效果。
我老老实实的打开performance,查看一下这地方的点击前后的性能分析,发现点击按钮后执行的时间来到了3秒,其中大部分时间都在执行微任务。

我再看处理函数的代码,突然明白了。
同一事件循环中大量微任务阻塞
因为循环中使用了await,当dictionaryList中有值的时候,await创建的Promise会在本轮宏任务中立刻resolve,这时候后续的代码会作为微任务进入到微任务队列中。等执行该微任务的时候,下一次的循环依然会产生新的微任务,也就是 执行微任务=>微任务生成微任务=>执行微任务 这样一个过程直到循环结束。而所有微任务都在同一个宏任务内连续执行,于是浏览器渲染被阻塞。
知道了原因,改正就简单了,只要先执行一次带有await的方法,然后通过同步函数获取就可以了
javascript
async function buildOptions(list) {
await getDict('')
for (let i = 0; i < list.length; i++) {
const dictList = getDictSync(config.dicType);
}
}
// 同步获取字典
function getDictSync(dicType) {
return this.dictionaryMap.get(dicType) || [];
}
再次查看这个地方的性能分析,总耗时已经降下来了,来到了100ms

关键点: 循环内对同步函数用 await,每个 await 都把下一次循环推迟到微任务队列,前一个微任务执行完又产生下一个微任务,所有微任务连续执行不中断,直到循环结束,浏览器才有机会渲染。