當(dāng)我們遇到需要一次性向頁(yè)面插入十萬(wàn)條數(shù)據(jù)的情況下,該如何保證頁(yè)面不卡頓爷光,維持一定的頁(yè)面渲染性能
?? 場(chǎng)景:
插入十萬(wàn)條數(shù)據(jù),渲染到頁(yè)面十萬(wàn)條數(shù)據(jù)
?? 分析:
我們知道,UI渲染在瀏覽器渲染進(jìn)程中屬于宏任務(wù)
瓦哎,且涉及到頁(yè)面的繪制,因此執(zhí)行完當(dāng)前的的腳本柔逼,進(jìn)入宏任務(wù)階段后蒋譬,同時(shí)由于數(shù)據(jù)量大,整個(gè)渲染耗費(fèi)時(shí)間較長(zhǎng)
?? 方案:
- 把數(shù)據(jù)分批插入到頁(yè)面
- 虛擬列表愉适,渲染應(yīng)該渲染的 ?(本篇內(nèi)容本文基于vue實(shí)例做介紹)
實(shí)際上思路就是犯助,在首屏渲染的時(shí)候,只加載可視區(qū)域內(nèi)的列表項(xiàng)维咸,當(dāng)發(fā)生滾動(dòng)的時(shí)候剂买,動(dòng)態(tài)計(jì)算:
- 當(dāng)前展示數(shù)據(jù)項(xiàng)列表
- 渲染列表的垂直偏移距離
- 更新整個(gè)全部數(shù)據(jù)占位容器的高度
<template>
<div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
<div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
<div class="infinite-list" :style="{ transform: getTransform }">
<div ref="items"
class="infinite-list-item"
v-for="item in visibleData"
:key="item.id"
:style="{ height: itemSize + 'px',lineHeight: itemSize + 'px' }"
>{{ item.value }}</div>
</div>
</div>
</template>
-
infinite-list-container
: 可視區(qū)域 -
infinite-list-phantom
:占位容器(生成滾動(dòng)條) -
infinite-list
:渲染區(qū)域,通過(guò)設(shè)置垂直偏移量癌蓖,模擬滾動(dòng)效果
1?? 等高的數(shù)據(jù)項(xiàng)
數(shù)據(jù)源model相關(guān)
-
itemSize
:數(shù)據(jù)項(xiàng)高度 -
startIndex
:可渲染區(qū)域的雷恃,起始索引值 -
endIndex
:可渲染區(qū)域的,結(jié)束索引值(起始 + 可渲染數(shù)據(jù)項(xiàng)visibleCount) -
visibleData
:可渲染區(qū)域的费坊,列表數(shù)據(jù) -
screenHeight
: 可是區(qū)域高度 -
startOffsetY
:可渲染區(qū)域的垂直偏移距離
更新視圖view相關(guān)
監(jiān)聽(tīng)container 可視區(qū)域的滾動(dòng)事件scroll時(shí)倒槐,拿到最新的scrollTop值,可以計(jì)算更新相關(guān)值:
- 起始索引
startIndex = Math.ceil(scrollTop / itemSize)
- 可渲染數(shù)量
visibleCount = Math.ceil(screenHeight / itemSize)
- 結(jié)束索引
endIndex = startIndex + visibleCount
- 可渲染數(shù)據(jù)
visibleData = listData.slice(startIndex, endIndex)
- 渲染區(qū)域偏移距離
startOffsetY = scrollTop - (scrollTop % itemSize)
然而在具體實(shí)現(xiàn)的時(shí)候附井,很多列表的各數(shù)據(jù)項(xiàng)讨越,是不定高度的;因此我們考慮實(shí)現(xiàn)動(dòng)態(tài)計(jì)算高度
2?? 動(dòng)態(tài)高度的數(shù)據(jù)項(xiàng)
- 默認(rèn)數(shù)據(jù)項(xiàng)高度
estimatedItemSize
用于初始化 - 記錄數(shù)據(jù)項(xiàng)位置和高度
positions
:
this.positions = this.listData.map((d, index) => ({
index,
height: this.estimatedItemSize,
top: index * this.estimatedItemSize,
bottom: (index + 1) * this.estimatedItemSize
}));
更新view視圖
監(jiān)聽(tīng)container 可視區(qū)域的滾動(dòng)事件scroll時(shí)永毅,拿到最新的scrollTop值把跨,可以計(jì)算更新相關(guān)值:
- 起始索引(動(dòng)態(tài)計(jì)算):
this.positions.findIndex(item => item.bottom > scrollTop)
- 可渲染數(shù)量
visibleCount : Math.ceil(screenHeight/ estimatedItemSize)
- 結(jié)束索引
endIndex : startIndex + visibleCount
- 可渲染數(shù)據(jù)
visibleData : listData.slice(startIndex, endIndex)
- 渲染區(qū)域偏移距離
startOffsetY
:this.positions.find(item => item.bottom > scrollTop)
這個(gè)起始項(xiàng)的top
值
更新model
每次渲染完成之后,在vue 的 updated鉤子($nextTick)里面處理更新和校對(duì)操作
-
校對(duì)positions: 通過(guò)拿到頁(yè)面dom數(shù)據(jù)項(xiàng)沼死,拿到節(jié)點(diǎn)高度着逐,遍歷數(shù)組與緩存的
positions
做對(duì)比:不同則需要更新該節(jié)點(diǎn)的position值(同時(shí),需要更新后續(xù)節(jié)點(diǎn)的值,因?yàn)楹笠豁?xiàng)的top
耸别,其實(shí)也是前一項(xiàng)的bottom
) -
校對(duì)滾動(dòng)條長(zhǎng)度: 更新占位容器
phantom-list
的實(shí)際高度:positions最后一項(xiàng)的bottom
-
校對(duì)渲染區(qū)域位置更新渲染區(qū)域
content-list
的垂直偏移量startOffsetY
然而從最終效果看健芭,在滑動(dòng)速度較快的情況下,仍然會(huì)出現(xiàn)空屏的情況...??
因此秀姐,我們考慮加上前后兩層緩沖區(qū)
慈迈,前后分別都添加上緩沖區(qū)數(shù)據(jù),計(jì)算visibleData
時(shí)省有,
// AVGSCALE 比如是 0.5
const startIndex = start - (visibleCount * AVGSCALE);
const endIndex = end + (visibleCount * AVGSCALE);
return this.listData.slice(startIndex, endIndex)
??查看動(dòng)態(tài)高度demo實(shí)現(xiàn)效果
??思考
目前的更新操作痒留,是放在scroll監(jiān)聽(tīng)事件中處理,這種高頻觸發(fā)方案蠢沿,難免會(huì)重復(fù)計(jì)算損耗性能伸头,可以考慮在intersectionObserver
這里監(jiān)聽(tīng),在回調(diào)方法里面處理相關(guān)的更新操作
這兩種方案目前是借助js操作的角度舷蟀,去優(yōu)化大數(shù)據(jù)量渲染的性能問(wèn)題恤磷;在css層面,也有相關(guān)的優(yōu)化方案雪侥,其中content-visibility
屬性碗殷,就是一個(gè)很有效的屬性(但是它的兼容不好??)