性能优化:树形组件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 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱12 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai21 分钟前
uniapp
前端·javascript·vue.js·uni-app
bysking1 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
萌面小侠Plus2 小时前
Android笔记(三十三):封装设备性能级别判断工具——低端机还是高端机
android·性能优化·kotlin·工具类·低端机
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205872 小时前
web端手机录音
前端