• 前端性能优化 - 虚拟滚动


    一 需求背景

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

    二 虚拟滚动原理

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

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

    在这里插入图片描述

    可以计算:卷入行数 = 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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    data() {
    	return {
    	  // 动态列-ETF精选
          overviewColumns: this.$columns.getColumns('etf_selected_list'),
          // 表格数据
          infoList: [],
          showInfoList: [],
          
    	  // 开始索引
          startIndex: 0,
          // 空元素,用于撑开table的高度
          vEle: undefined,
          // 每一行高度
          itemHeight: 42,
     	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    watch: {
        sliceTable: {
          handler() {
          // 解决表格错位问题
            this.$nextTick(() => {
              this.$refs.latestPositionRef.doLayout()
            })
          },
          deep: true
        },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
      async created() {
        // 创建一个空元素,这个空元素用来撑开 table 的高度,模拟所有数据的高度
        this.vEle = document.createElement('div')
      },
    
    • 1
    • 2
    • 3
    • 4
      computed: {
        // 这个是截取表格中的部分数据,放到了 table 组件中来显示
        sliceTable() {
          return this.showInfoList.slice(this.startIndex, this.startIndex + 6)
        },
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 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()
           // })
         }
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    四 其他方案:

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

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

  • 相关阅读:
    代码随想录 | Day 51 - LeetCode 309.最佳买卖股票时机含冷冻期、LeetCode 714.买卖股票的最佳时机含手续费
    高等数学(第七版)同济大学 习题10-2(中5题) 个人解答
    el-date-picker日期选择器奇怪的问题解决
    ubuntu18.04 禁用自带nouveau后重启无法进入系统
    ChatGPT 论文助手:如何用 AI 技术加速学术写作过程
    算法学习:Leetcode-623. 在二叉树中增加一行
    LC-6248. 统计中位数为 K 的子数组(回文:中心扩散+哈希、等价转换)【周赛321】
    上周热点回顾(7.18-7.24)
    神经网络结构图绘图软件,大脑神经网络结构图片
    初阶数据结构(6)(队列的概念、常用的队列方法、队列模拟实现【用双向链表实现、用数组实现】、双端队列 (Deque)、OJ练习【用队列实现栈、用栈实现队列】)
  • 原文地址:https://blog.csdn.net/Xxxxxl17/article/details/134001855