性能优化:虚拟列表,如何渲染10万条数据的dom,页面同时不卡顿

  • 2019 年 10 月 3 日
  • 笔记

最近做的一个需求,当列表大概有2万条数据,又不让做成分页,如果页面直接渲染2万条数据,在一些低配电脑上可能会照成页面卡死,基于这个需求,我们来手写一个虚拟列表

思路

  1. 列表中固定只显示少量的数据,比如60条
  2. 在列表滚动的时候不断的去插入删除dom
  3. startIndex、endIndex,不断的改变这个值来获取最新的显示列表
  4. paddingTop、paddingBottom撑开容器的滚动区域

首先看一下当直接插入2万条列表时,页面的性能

可以看到火焰图中已经有了红色的部分了,dom渲染也耗时也有1s多

再来看一下当使用虚拟列表时页面的性能

从火焰图中可以看出,火焰图中一篇绿油油的,这就证明,通过虚拟列表来进行渲染使页面性能得到了极大的提升

简单的虚拟列表demo实现

我们假设有一个容器,高度为600px,列表项每个高度为30px,那么根据列表的length我们就可以计算出滚动容器的总高度,也可以知道显示60条数据的高度,我们此时可以给容器加一个paddingBottom,来撑开容器,来模拟页面应该滚动的高度

this.paddingBottom = this.allHeight - this.scrollList.length * 30

容器同时还需要paddingTop用做当容器滚动顶部数据移除后撑起scrollTop

最后我们需要监听容器的滚动事件来不断的修改paddingTop、paddingBottom、startIndex、endIndex

最终效果

最后附上所有代码

<!doctype html>  <html lang="zh">  <head>      <meta charset="UTF-8">      <meta name="viewport"            content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">      <meta http-equiv="X-UA-Compatible" content="ie=edge">      <title>Document</title>      <style>          .container {              width: 300px;              height: 600px;              overflow: auto;              border: 1px solid;              margin: 100px auto;          }          .item {              height: 29px;              line-height: 30px;              border-bottom: 1px solid #aaa;              padding-left: 20px;          }      </style>  </head>  <body>  <div id="app">      <button @click="add">增加</button>      <div class="container" ref="container">          <div class="scroll-wrapper" :style="style">              <div v-for="(item, index) in scrollList" :key="index" class="item">{{item}}</div>          </div>      </div>  </div>  <script src="./vue.js"></script>  <script>      new Vue({          el: '#app',          data: {              list: [                  '测试数据'              ],              startIndex: 0,              endIndex: 60,              paddingTop: 0,              paddingBottom: 0,              allHeight: 0          },          computed: {              scrollList() {                  return this.list.slice(this.startIndex, this.endIndex)              },              style() {                  return {                      paddingTop: this.paddingTop + 'px',                      paddingBottom: this.paddingBottom + 'px'                  }              }          },          watch: {              list(val) {                  const valLen = val.length                  this.allHeight = valLen * 30                  this.paddingBottom = this.allHeight - this.scrollList.length * 30              }          },          mounted() {              const container = this.$refs.container              container.addEventListener('scroll', () => {                  const top = container.scrollTop                  this.startIndex = Math.floor(top / 30)                  this.endIndex = this.startIndex + 60                    this.paddingTop = top                  if (this.endIndex >= this.list.length - 1) {                      this.paddingBottom = 0                      return                  }                  this.paddingBottom = this.allHeight - 600 - top              })          },          methods: {              add() {                  let arr = new Array(50000).fill(0)                  arr = arr.map((item, index) => {                      return index                  })                  this.list = [                      ...this.list,                      ...arr                  ]              }          }      })  </script>  </body>  </html>