前端性能优化 - 虚拟滚动

一 需求背景

需求:在一个表格里面一次性渲染全部数据,不采用分页形式,每行数据都有Echart图插入。
问题:图表渲染卡顿
技术栈:Element UI
卡顿原因:页面渲染时大量的元素参与到了重排的动作中,性能差
解决办法:虚拟滚动

二 虚拟滚动原理

虚拟滚动其实就是综合数据分页和无限滚动的方法,在有限的视口中只渲染我们所能看到的数据,超出视口之外的数据就不进行渲染,可以通过计算可视范围内的单元格,保证每一次滚动渲染的DOM元素都是可以控制的,不会担心像数据分页一样一次性渲染过多,也不会发生像无限滚动方案那样会存在数据堆积,是一种很好的解决办法。

假设实际开发中服务端一次响应20万条列表数据,此时设备屏幕只允许容纳20条,那么用户理论上只可以看见20条数据,其他的数据不会进行渲染加载。如果前端将20万条数据全部渲染成DOM元素,可能造成程序卡顿,占用较大资源,非常影响用户体验,那么虚拟滚动技术就完美的解决了这一问题。

前端性能优化 - 虚拟滚动_第1张图片

可以计算:卷入行数 = scrollTop(卷入高度) / 每行的高度(itemH)

如何计算可视区域渲染的元素以及实现虚拟滚动,步骤如下:

  • 统一设置每一行的高度需要相同,方便计算。
  • 需要计算渲染数据数量(数组的长度),根据每行的高度以及元素的总量计算整个DOM渲染容器的高度。
  • 获取可视区域的高度
  • 触发滚动事件后,计算偏移量(滚动条据顶距离),再根据可视区域高度计算本次偏移的截止量,得到需要渲染的具体数据。
  • 对于与表格的列来说,需要做虚拟滚动的话,在x轴同样可以根据以上步骤执行,实现横向虚拟滚动。

三 项目具体代码:

  <el-table
     ref="latestPositionRef"
     v-loading="tableLoading"
     class="table-fixed"
     size="mini"
     :data="sliceTable"
     height="355px"
     :cell-style="cellStyle"
     row-key="secId"
     @sort-change="handleSortChange"
     @selection-change="handleSelectionChange"
   >
     <el-table-column type="selection" width="55" align="center" :reserve-selection="true" />
     <el-table-column label="排名" width="50px" align="center" fixed>
       <template slot-scope="scope">{{ scope.$index + 1 }}</template>
     </el-table-column>
     <DynamicColumn
       v-for="(ite, index) in overviewColumns"
       :key="index"
       :item="ite"
       :empty="1"
       :data-list="infoList"
       table-sign="latest-position-list"
       :schemas="overviewColumns"
       @changeColumn="(cols)=>{overviewColumns=cols;}"
     />
     <el-table-column label="备注设置" align="center" width="50" fixed="right">
       <template slot-scope="scope">
         <el-button
           v-if="scope.row.secId"
           type="text"
           size="mini"
           plain
           @click="setRemark(scope.row)"
         >查看</el-button>
       </template>
     </el-table-column>
   </el-table>
data() {
	return {
	  // 动态列-ETF精选
      overviewColumns: this.$columns.getColumns('etf_selected_list'),
      // 表格数据
      infoList: [],
      showInfoList: [],
      
	  // 开始索引
      startIndex: 0,
      // 空元素,用于撑开table的高度
      vEle: undefined,
      // 每一行高度
      itemHeight: 42,
 	}
}
watch: {
    sliceTable: {
      handler() {
      // 解决表格错位问题
        this.$nextTick(() => {
          this.$refs.latestPositionRef.doLayout()
        })
      },
      deep: true
    },
}
  async created() {
    // 创建一个空元素,这个空元素用来撑开 table 的高度,模拟所有数据的高度
    this.vEle = document.createElement('div')
  },
  computed: {
    // 这个是截取表格中的部分数据,放到了 table 组件中来显示
    sliceTable() {
      return this.showInfoList.slice(this.startIndex, this.startIndex + 6)
    },
  }
 /** 加载ETF精选列表数据 */
   loadData() {
     console.log('loadData')
     const start_i = this.showInfoList.length
     for (let i = start_i; i < start_i + 10; i++) {
       this.showInfoList.push(this.infoList[i])
     }
     this.$nextTick(() => {
       // 设置成绝对定位,这个元素需要我们去控制滚动
       this.$refs.latestPositionRef.$el.querySelector(
         '.el-table__body'
       ).style.position = 'absolute'
       // 计算表格所有数据所占内容的高度
       this.vEle.style.height =
         this.showInfoList.length * this.itemHeight + 'px'
       // 把这个节点加到表格中去,用它来撑开表格的高度
       this.$refs.latestPositionRef.$el
         .querySelector('.el-table__body-wrapper')
         .appendChild(this.vEle)
       // 重新设置曾经被选中的数据
       this.selection.forEach((row) => {
         this.$refs.latestPositionRef.toggleRowSelection(row, true)
       })
     })
   },

   tableScroll() {
     console.log('tableScroll')
     const bodyWrapperEle = this.$refs.latestPositionRef.$el.querySelector(
       '.el-table__body-wrapper'
     )
     console.log(bodyWrapperEle, 'bodyWrapperEle')
     // 滚动的高度
     const scrollTop = bodyWrapperEle.scrollTop
     // 下一次开始的索引
     this.startIndex = Math.floor(scrollTop / this.itemHeight)
     // 滚动操作
     bodyWrapperEle.querySelector(
       '.el-table__body'
     ).style.transform = `translateY(${this.startIndex * this.itemHeight}px)`
     // 滚动操作后,上面的一些 tr 没有了,所以需要重新设置曾经被选中的数据
     this.selection.forEach((row) => {
       this.$refs.latestPositionRef.toggleRowSelection(row, true)
     })

     // 滚动到底,加载新数据
     if (
       bodyWrapperEle.scrollHeight <=
       scrollTop + bodyWrapperEle.clientHeight
     ) {
       if (this.showInfoList.length === this.infoList.length) {
         this.$message.warning('没有更多了')
         return
       }
       this.loadData()
       // 解决el-table中内容错位
       // this.$nextTick(() => {
       //   this.$refs.latestPositionRef.doLayout()
       // })
     }
   }

四 其他方案:

分段渲染、数据分页、无限滚动

学习:https://www.modb.pro/db/122781

你可能感兴趣的:(1024程序员节,性能优化,前端)