性能优化 | el-table中内嵌大量el-input控件导致渲染卡顿的问题

6,893 阅读2分钟

场景

项目中有一个应用场景,用户需要在表单中大量使用选择框以及输入框填写数据(每一行大概有三十几个输入框),当选择框与输入框达到一定数量的时候,页面会出现输入不连续、卡顿的现象,如下图: 需求原型.png

排查

查了一下网上的解决方案,大体是因为页面渲染过多的el-input输入框给浏览器带来较大的渲染压力导致的。基于这点,对项目作出优化。

引入虚拟列表

虚拟列表,简单来说就是页面并不渲染表格中的所有数据,只渲染可视窗口部分的数据,当用户拖动滚动条滚动的时候,逐渐加载数据。这个适用于表格中需要渲染大量数据的场景,从而减轻浏览器的渲染压力。

对于elementui中使用el-table加入虚拟列表,可以参考下面这篇文章: element全网最简单el-table实现虚拟列表

参考上面的文章,做了一个初版的效果。但由于虚拟列表只渲染部分数据的特性,所以一定会带来一个缺陷,那就是会短暂出现白屏的问题,即用户看到未渲染的区域。排查原因得知是因为el-table的占位区域append设置为定高totalHeight

<!-- 占位 -->
<template #append>
  <div :style="{ height: `${totalHeight}px` }"></div>
</template>

...
totalHeight() {
  return this.tableData.length * this.itemHeight
}

经过思考,改成动态变化的高度,从而形成一种滚动触底更新的效果:

// 改变append的高度
updated() {
  if (this.$refs.multipleTable) {
    const bodyEl = this.$refs.multipleTable.$el.getElementsByTagName("tbody")[0]
    if (bodyEl.offsetHeight) {
      this.totalHeight = bodyEl.offsetHeight
    } else if (bodyEl.offsetHeight === 0 && this.tableData.length === 0) {
      this.totalHeight = 0
    }
    // 重新绘制表格
    this.$refs.multipleTable.doLayout()
  }
},

虚拟列表再优化

引入虚拟列表后,加载确实快不少,输入卡顿的显现也明显优化。但发现一个问题,原有的选择方法不生效,当用户滚动的时候,原有的选择项被清空。那么,如何记录选择列表的状态呢?elementui提供了一个reserve-selection的API,用于记录列表项的选择状态: reserve-selection.png

当然,也需要重写@select="handleSelect"@select-all="handleSelectAll"方法:

// 监听选择全部
handleSelectAll() {
  if (this.isSelectAll) {
    this.tableData.forEach(item => {
      this.$refs.multipleTable.toggleRowSelection(item, false)
    })
    this.selectList = []
    this.isSelectAll = false
  } else {
    this.tableData.forEach(item => {
      this.$refs.multipleTable.toggleRowSelection(item, true)
    })
    this.selectList = this.tableData
    this.isSelectAll = true
  }
},
// 手动触发选择事件
handleSelect(selection) {
  this.selectList = selection
  if (selection.length === this.tableData.length) {
    this.isSelectAll = true
  } else {
    this.isSelectAll = false
  }
},

引入虚拟输入框

引入虚拟列表后,效果确实提升了不少,至少在大数据量的情况下,不再会出现卡顿了(因为大数据量其实只渲染固定的数据)。但还有可以优化的空间吗? 我们之前提到,造成页面卡顿的原因,就是大量渲染了el-input以及el-select控件。那么,可以再进一步优化,以减少控件的渲染数量吗?我在这篇文章中找到了灵感: vue大数据表格卡顿问题的完美解决方案 或者直接看文章提到的demo: Demo DEMO.png 核心思路就是,**只有当需要编辑的时候,才显示输入框,其余都显示内容。**这么一来,最多只会渲染1个编辑控件,卡顿问题可以进一步得到优化。

实现思路

  • template中根据条件选择渲染编辑控件,默认显示表格内容(同时通过修改样式模拟输入框必填标红);
  • 鼠标点击表格的时候,记录当前表格的坐标,用于切换编辑控件;
  • 页面根据渲染条件,切换编辑控件,同时自动聚焦到编辑框中。

示例代码如下所示:

<el-form-item :prop="'displayList.' + $index + '.出差地点'">
  <el-input v-if="focusCell === `${$index}-出差地点`" type="textarea" v-model="row['出差地点']" @blur="beforeComputedAll" autosize></el-input>
  <div v-else :class="row['出差地点'] ? 'm-input' : 'custom-required'">{{ row['出差地点'] }}</div>
</el-form-item>

记录表格坐标可以使用el-table的@cell-click方法:

// 聚焦单元格
updateFocusCell(row, col, cell) {
  if (this.focusDebounce) {
    clearTimeout(this.focusDebounce)
    this.focusDebounce = null
  }
  this.focusDebounce = setTimeout(() => {
    const lineNo = row['行号']
    const prop = col.property
    const idx = this.displayList.findIndex(item => item['行号'] === lineNo)
    if (this.focusCell !== `${idx}-${prop}`) {
      this.focusCell = `${idx}-${prop}`
      this.$nextTick(() => {
        target.focus()
      })
    }
    this.focusDebounce = null
  }, 20)
},

最终效果

最终效果.jpg

总结

至此,表格已经从一开始的输入卡顿,到现在基本是可以流畅输入了,使用体验得到大大的提升。当然还有优化的空间,比如校验逻辑的编写,以及一些相关的计算等等,那块就是属于算法层面的东西了,在此就不展开叙述。 对于表格大批量数据渲染的优化方案,总结如下:

  • 引入虚拟列表,只渲染可视化区域的数据;
  • 模拟编辑控件,仅在需要编辑的时候才展示;
  • 计算逻辑优化,减少计算时间。

第一次遇到这种类型的优化,觉得挺有意思的,在此做一个记录。如果大家有更好的优化方案也可以在评论区提出,欢迎大家一起学习~