性能优化:树形组件tree一次针对大量数据的优化。

有一天体验公司项目,录制performance时候,发现一段预期之外longtask,是一个筛选项,使用ui库的treeSelect组件渲染的(大体样式如下面截图),点这个筛选项在其进行渲染时有一段比较夸张的longtask,耗时高达600ms左右。我的电脑配置是16核32G的M2芯片,在我电脑上600ms,按照经验在用户电脑上耗时得接近乘3,那得1.5s左右的耗时了,明显影响使用体验了,需要解决掉这个问题。

一:问题分析

  1. 对于一个treeSelect组件,渲染耗时较长,大概率就是因为数据太多了。所以首先我看了下接口返回的筛选项数据,确实是一个比较大的json。接口返回的数据中此筛选项高达900kb(解压缩后的大小)。初步猜测得到确认。
  2. 我将接口数据mock到本地,然后本地performance录制。mock到本地是因为本地开发由于有sourcemap信息,可以更清楚的看调用栈的文件信息跟函数信息。经过观察performance,发现调用栈中的treeSelect组件下的getDerivedState方法中的needUpdate方法中的isEqual极其耗时,光isEqual部分耗时就400ms左右。此处代码应该是用来判断组件render时,当前props传进来的treeData跟上一次的treeData相比数据是否发生变化,如果数据发生变化了才会reder。如果treeData是一个相当大的对象,那么在进行比对时候就会层层递归比对。看调用栈底部的调用情况,也会发现各种equal相关的递归调用。
  3. 至此问题基本确认,看起来是大对象在使用lodash的isEqual来进行深度对比时候,耗时较多。
  4. 可是问题基本定位到之后,我的疑问来了。isEqual真的性能这么差吗?一个900k大小的json,比对在m2的cpu下需要400ms+?不太相信。然后把mock的数据拿出来,简单写了个demo,只使用lodash的isEqual进行两个大对象的比较,果然发现只有20ms左右。为什么在我们这就需要400ms了呢?需要进一步找到这两个场景的不同点。

5. 开始找不同,在我们项目里通过在isEqual处打断点发现,传过来的treeData是经过mobx包装后的带有proxy的对象,而我们写的demo验证里,用的却是用的未经过proxy代理的对象。这是一个很重要的差异,尤其在大对象递归取值的时候。 6. 将对象改为mobx的非可观察对象后,isEqual的耗时直接减半了。从400ms+减少到200ms。 7. 可是200ms依然离着demo里的20ms相差甚远啊。只能看treeSelect的源码了,getDerivedState部分,搜了一下关键词needUpdate('treeData'),这段是用来比较treedata是否发生变化的,发现在getDerivedState里居然调用了三次needUpdate('treeData'),理论上只需要调用一次将结果放到一个变量里,下次直接使用此结果就可以了。 8. 直接在node_modules里的改treeSelect的源码,声明一个变量将needUpdate('treeData')的比对结果存储下来,后续直接使用此结果信息。重新运行longtask这次终于降下来了。此段整体400ms的longtask降到50ms左右了,其中isEqual部分也在20ms左右了,符合预期了。

二:问题解决

此段lontask的由来主要是三个原因:

  1. 是一个大对象
  2. 这个大对象经过mobx包装,给对象递归的加了层层代理proxy。取值时候自然多一层。
  3. ui库的treeSelect组件里在进行treeData的isEqual比较时候,三次调用。

针对以上问题,解决办法也比较简单了:

  1. 将此筛选项的大对象,设置为不可观察,去掉proxy。
  2. 组件库对treeSelect组件进行修复,将三次isEqual的调用改为一次调用即可。

三:额外疑问

  1. 在needUpdate函数里,明明调用了三次isEqual,可是为什么看performance调用栈里只有一个isEqual呢,理论上应该是并排的三个isEqual调用栈才对。而实际performance录屏中。也确实偶然出现过三个isEqual并排的情况。这是什么原因呢。两次performance的录制不同如下。图一是三个isEqual并排,每个耗时大约140ms,图二是只有一个isEqual耗时大约450ms。

2. 如果调用栈信息能保持预期内的三个isEqual并排,我是不是就早就轻易发现isEqual调用了三次的问题啊?这样对于问题排查造成了干扰。 3. 针对此疑问,我在问题解决后找时间搜了一些相关"火焰图生成原理"的文章,得到一个感觉相对合理的答案,能解释的通三个isEqual合并成一个的情况。大概率是由于火焰图数据的采样频率引起的,采样频率一般以赫兹为单位,比如采样频率1000HZ,代表一秒钟采样1000次。假如当前函数中其他的指令的执行时间太短,在执行过程中并没有命中采样,则相当于没有此段数据,则会出现调用栈合并的情况。我自己也写了一个demo验证了下此情况。

bash 复制代码
 3-1:test函数中,先后调用了longtask,shorttask,longtask,如果shorttask执行时间比较短,执行中恰好cpu没进行采样,那么就会出现两段longtask合并为一段的情况。
 

3-2:如果shorttask的执行时间稍长,尽量让其执行时候肯定会被cpu采样到,那么两段longtask就会被分开了。如下图。

js 复制代码
<div>
        <button onclick="test()">点我</button>
</div>
<script>
        
        function longtask () {
                const arr = [];
                for (let i = 0; i < 1000 * 10000; i++) {
                        arr.push(i);
                }
        }
        
        function shorttask () {
                const arr = [];
                for (let i = 0; i < 10 * 10; i++) {
                        arr.push(i);
                }
        }
        
        function test() {
                longtask();
                shorttask();
                longtask();
        }
</script>
相关推荐
崔庆才丨静觅9 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了10 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅10 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅10 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment11 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅11 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊11 小时前
jwt介绍
前端
爱敲代码的小鱼11 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax