push竟比concat快上数百倍?记一个concat在十万级数据引发的性能问题
壹 ❀ 引
公司产品一直在做企业项目研发工具,所以我们自己当然也会用自己的产品去管理公司大小项目,但在此之前,项目管理体验上一直存在一个卡顿问题。比如我刚登录上账号,在项目里随便到处点点到处跳转页面,然后点击项目头部的搜索功能进行任意搜索,并成功跳转到搜索结果页后,再点击chrome
的回退按钮回到上个页面,就会遇到长达10S的页面卡顿,我的电脑是16G M1芯片都要卡这么久,像测试同学配置相对差一点的MAC,chrome
甚至会卡到直接失去响应,总而言之,如果客户碰巧也这么操作了,使用体验自然非常很好。
贰 ❀ Performance性能分析
好在chrome
已经提供了Performance
帮助我们分析页面加载性能瓶颈问题。F12
打开控制台,点击Performance
按钮就能看到如下界面,考虑到我的电脑配置较好,为了模拟低配置,更好的复现问题,所以我将CPU
选项选择为降低6倍性能6x slowdown
,另外,上面还有个Network
用于降网速,这个可用来模拟网络慢的情况,因为我这问题跟网络没啥关系,这里就不管了。
然后我还是按照上面说的操作,在项目里乱点一通,到处跳转,之后搜索,进入搜索页面,这时候就可以开启Performance
的录制功能,也就是上图那个黑色的圆形按钮,然后点击chrome
的回退按钮,卡顿几十秒后页面终于恢复正常,我们再点击录制按钮结束录制,少许片刻,于是我们得到了如下信息:
让我们把目光看向CPU的火焰图,从5000ms到60000ms这么长长的一段,接近55秒的时间内CPU使用都占了一半(黄色区域),那么性能问题自然出现在这55s之间。直接看一眼下面的统计报表,也能发现总共1.1min中,有62446ms在执行JS,而等待,渲染,重绘都在百ms,因此跟这些没太大关系。
我们拖拽鼠标,将分析范围选中为有问题的55S之间,可以看到是一个Task任务总共耗时是54秒,注意,这是一个长任务,也就是单一跑完这个任务就用了这么久。顺带解释下这段黄绿蓝区域的含义,横向表示这个任务耗时的长度,长度越长说明用时越久,纵向表示这个任务的调用栈,比如总任务名为Task,task下又分别包含了哪些任务呢?于是就有了纵向这样一列。
由于任务过长,我们不得不继续滚动鼠标,将选中范围精确到更小的执行粒子上,神奇的事情发生了,随着我选中范围越来越小,下面的调用栈名称居然就没变过,也就是说这么长的时间里,执行的JS过程就是如下这么一段,我们前面说了,纵向表示调用栈,而在最下的anonymous
下,存在无数个黄色的小段落,粗略来看,这个方法估计被执行了上千或者上万遍。
既然问题出在这,我们通过鼠标选中这个匿名函数,在Sunmary表报处我们就能看到这个函数所在的文件了,一个名为selector.js
的文件,点击进入,成功找到了可能有问题的代码:
看样子写这块代码的同学估计也猜到这里特别耗性能,所以才使用了memoize
做了缓存,只是没想到第一次缓存准备数据还是要等待几十秒。出于好奇,我在这段代码前后加了console.time
与console.timeEnd
,刷新,重新走一遍复现流程,看了眼控制台,人傻了...接近46秒,好奇看了眼这里的permissionRuleListMap
,居然有十几万的数据。
叁 ❀ 优化思路
怎么优化呢?其实文章标题已经给了答案了,就是一个小小的concat
引发的问题,这段代码第一获取了名为permissionRuleListMap
的所有key,然后遍历key,依次去取key对应的内容,再利用concat
将内容加入到新数组resultList
取,事实上这里都不需要做Object.keys
这部操作,毕竟Object.keys
也是一次遍历,十几万的数据跑一遍也需要时间,我们完完全全可以一遍遍历搞定,改为如下代码:
const resultList = [];
// 一次遍历,不用单独获取key
for (const key in permissionRuleListMap) {
// 用push取代concat
resultList.push(...permissionRuleListMap[key]);
}
return resultList;
重走上述流程,然后继续点回退,神奇的事情发生啦,看一眼控制台时间输出,现在只要几百毫秒了。
肆 ❀ 谨慎使用concat
为了更好的验证这两个方法的快慢,我们可以控制变量,声明一个包含10W个[1,2]
的数组,并将其复制到一个新数组中去,让我们来对比时间:
const arr = new Array(100000).fill([1, 2]);
let a1 = [];
let a2 = [];
// 使用concat
console.time('1');
for (let i = 0; i < arr.length; i++) {
a1 = a1.concat(arr[i]);
};
console.timeEnd('1');
// 使用push
console.time('2');
for (let i = 0; i < arr.length; i++) {
a2.push(...arr[i]);
}
console.timeEnd('2');
我们都知道,concat
是返回一个新数组,所以每次遍历,JS都需要新开一个内存,创建一个新数组,用于保存合并后的数组内容,所以遍历10W次,那么就需要重复开10W个内存,再将数组元素依次放进去,只要数组够大,它的时间只会要的更久。
相对而言push
就简单了,即便执行10W次,我们操作的始终是一个数组,每次我们都只是在这个数组上新加几个空位,用于依次存放新的数组元素而已,脑补一下这个过程,性能谁更优一目了然。
在Javascript Array.push is 945x faster than Array.concat 🤯🤔一文中,也有阐述为啥push
比concat
快了接近945倍,这里我们只需要得知这个结论就好,总而言之,请谨慎使用concat
方法,如果你的数据量较大,就一定得留意这一点,那么本文就记录到这里啦。