性能優化:虛擬列表,如何渲染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>