1. 程式人生 > >效能優化:虛擬列表,如何渲染10萬條資料的dom,頁面同時不卡頓

效能優化:虛擬列表,如何渲染10萬條資料的dom,頁面同時不卡頓

最近做的一個需求,當列表大概有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>