记一次vue性能优化实战:从51秒优化到3秒
编辑记一次vue性能优化实战:从51秒优化到3秒
这周前端组同事休假,所以我去顶替,万万没想到,开幕雷击,刚进组就有业务反馈一个页面的弹框卡顿,需要优化
计算机科学领域的任何问题都可以通过增加一个中间层来解决。 —— Butler Lampson
发现问题
点进去这个页面,页面本身支持500条的表格数据展示,然后可以选中整个表格,点击操作按钮,就会在弹框中重新渲染一个相同条数的表格,字段有略微的不一样,多了一些表格内的输入框和批量操作按钮。
此时,卡顿分为两个阶段:
-
第一个是点击按钮到打开弹框的阶段,选中500条数据的情况下,该页面会卡死51秒才有反应
-
第二个是弹框打开后渲染表格后的阶段,渲染的阶段倒是挺快的,但是渲染出来的表格基本不能用,上下滚动都很卡
寻找原因
先说上面的第二个阶段,就是渲染出来的表格卡顿的问题:
这个很好定位原因,原因就是外层的表格和弹框的表格都是500行数据,用的是iview的table组件,这个table组件没有虚拟滚动的功能,所以基本上就是1000行的表格全部加载出来了,而且表格内还有额外的dom结构和操作事件等等,当然卡了。
然后是第一个阶段,由于是弹框显示前的卡顿,所以要找找在点击按钮到弹框显示的这一段旅程,到底发生了什么:
选中500条数据,点击上面的修改价格
按钮,执行了如下代码
batchStockandPrice(title) {
if (this.calInfoSelected.length <= 0) {
this.$Message.warning('请选择要修改的产品')
} else {
this.showPriceModal = true
this.title = title
}
},
附上部分的template:
<Button type="primary" @click="batchStockandPrice('修改库存')" :loading="loading">修改库存</Button>
<Button type="primary" @click="batchStockandPrice('修改价格')" :loading="loading">修改价格</Button>
<Button type="primary" @click="batchStockandPrice('修改重量')" :loading="loading">修改重量</Button>
<Button type="primary" @click="batchStockandPrice('修改包装尺寸')" :loading="loading">修改包装尺寸</Button>
<batchPrice v-model="showPriceModal" :selectTable="calInfoSelected" :title="title" :allCountryList="allCountryList"></batchPrice>
可以见逻辑是,点击按钮,将showPriceModal
设为ture
,传入的数据是选择的表格数据,然后显示批量修改的弹框,这里发现这个弹框竟然承载了4个功能。
然后这里没啥好优化的,所以进入这个batchPrice
组件看看显示前都在做什么:然后我就发现了这个可怕的循环嵌套函数:
无需关心代码具体在执行什么逻辑,光是看这几个红色的嵌套循环,我就感觉有点震惊了。于是我在循环前记了一下数,然后循环完毕打印出来,在200条量级的情况下,循环了2w次左右:
而在500条数据的量级下,循环膨胀到了12w次:
但是按理来说,cup搞定这点循环,不应该耗时51秒那么长时间。
想到vue相关的问题,最多的就是响应式变量的问题,所以再分析如上代码,看有没有其它的优化点,结果这个时候发现,这段代码不仅仅在循环,而且在循环中不断的给响应式的变量push值进去,然后又循环更改响应式变量的值,如下蓝色的标注:
不断循环更新响应式变量,vue就会不断的追踪更改,最终导致的结果就是成千上万的更新响应式变量,带来的是指数级上涨的依赖更改追踪,带来的直观的后果就是页面卡死了51秒才计算完毕。
解决问题
第一个阶段的解决方案
先不优化代码本身的逻辑,避免遗漏逻辑或者写出bug,而是使用一个非响应式的temp变量,来代替上面的this.tableDataStock
,然后在所有的循环完毕后,将这个temp赋值给this.tableDataStock
即可:
第二个阶段的解决方案:
-
先观察Table组件和vxe-grid组件的所需配置的差异
-
Table组件和vxe-grid都是用columns接收表格配置,用data接收表格数据,这点很好,不用特别设置了
-
columns的配置有一些差异,稍微总结了一下如下:
Table vxe-grid 备注 key field slot: xxx slots: {
default: xxx
}render slots: {
default: xxx
}vxe-grid在v2版本接收的 default
字段如果是VNode,好像必须是数组
该字段由于此次适配没有用到,所以没有测试过,但是理论是可行的。
-
-
然后写一个中间件组件EsGrid,它接收Table一模一样的传参和插槽,内部将这些传参和插槽处理成vxe-grid能识别的产物,然后传递给vxe-grid即可;
关于插槽的处理:
<template> <div> <vxe-grid :columns="computedColumns" v-bind="$attrs" v-on="$listeners"> <template v-for="slotName in Object.keys(slots)" #[slotName]="{ row, index }"> <slot :name="slotName" :row="row" :index="index"></slot> </template> </vxe-grid> </div> </template>
这里两点需要注意的是:
- $slots不知道为啥没生效,所以使用了vue2.7的hook
useSlots()
来获得的slots - vue2.7版本,需要同时使用
v-bind="$attrs" v-on="$listeners"
来继承父组件传来的属性和事件,而vue3不用,直接v-bind="$attrs"
就可以同时继承父组件传来的属性和事件
关于columns的适配:
const computedColumns = computed(() => { if (isIViewTable()) { // isIViewTable判断是否是iview的表格columns return props.columns.map(item => { const newSlots = {} if (item.slot) { newSlots.default = item.slot } return { ...item, // 先继承所有的属性 field: item?.key, // 转换key为field slots: item.slot ? newSlots : undefined // 转换slot为vxe能识别的slot } }) } else { return props.columns } })
- $slots不知道为啥没生效,所以使用了vue2.7的hook
-
最后直接使用EsGrid替换Table组件,无需改动任何其它的东西。
优化结果
经过上述的步骤,在选中500条数据的情况下,该弹框打开的速度从原本的51s骤降为3s左右,效率提升了1700%。
如果还需要对这3s进行优化的话,就得改写上面的嵌套循环的逻辑了,但是emmm,下次一定!
总结
关于性能方面的总结:
- 尽量使用性能表现优异的组件,比如使用自带虚拟滚动的vxe-table代替iview自带的table组件
- 避免在循环中重复的给响应式对象赋值! 正确的做法是先给临时的非响应式变量赋值,然后在循环完毕后将这个临时变量一次性赋值给响应式对象
关于组件封装方面的总结:
跟我之前保存的一篇文章所说的一样:添加一个中间层是一种有效且通用的解决问题的思路,根据需要解决的问题,思考中间层的位置,以及一个能注入代码的时机,并让代码尽早执行
所以这里我就采用了封装一个中间层的组件EsGrid,对外接收插槽和props,对内处理这些插槽和props:将这俩货转为vxe支持的格式,并提供给vxe使用即可。
关于vue2.7的总结:
-
在template里面直接使用
$slots
不能正确的拿到插槽,所以要在生命周期里面,使用useSlots()
hook来获取到slot,然后在template里面v-for遍历即可- 同样的
getCurrentInstance
之类的hook也需要在vue的生命周期里使用,比如setup函数或者created,或者onMounted等等,不然会返回null
- 同样的
附录
https://hughfenghen.github.io/posts/2023/12/23/web-spy/
- 0
- 0
-
分享