小程序長列表性能優(yōu)化實(shí)踐
點(diǎn)擊上方關(guān)注 前端技術(shù)江湖,一起學(xué)習(xí),天天進(jìn)步

作者:lmq1919
https://juejin.cn/post/6966904317148299271
某天閑著無聊想練一下手速,去上拉一個(gè)小程序項(xiàng)目中一個(gè)有1萬多條商品數(shù)據(jù)的列表。在數(shù)據(jù)加載到1000多條后,是列表居然出現(xiàn)了白屏。看了一下控制臺:

‘Dom limit exceeded’,dom數(shù)超出了限制, 不知道微信是出于什么考慮,要限制頁面的dom數(shù)量。
一.小程序頁面限制多少個(gè)wxml節(jié)點(diǎn)?
寫了個(gè)小dome做了個(gè)測試。listData的數(shù)據(jù)結(jié)構(gòu)為:
listData:[
{
isDisplay:true,
itemList:[{
qus:'下面哪位是劉發(fā)財(cái)女朋友?',
answerA:'劉亦菲',
answerB:'迪麗熱巴',
answerC:'齋藤飛鳥',
answerD:'花澤香菜',
}
.......//20條數(shù)據(jù)
]
}]
頁面渲染效果:

1.dome1
<view wx:for="{{listData}}" class="first-item" wx:for-index="i" wx:for-item="firstItem" wx:key="i" wx:if="{{firstItem.isDisplay}}">
<view class="item-list" wx:for="{{firstItem.itemList}}" wx:key="index">
<view>{{item.qus}}</view>
<view class="answer-list">
<view>A. <text>{{item.answerA}}</text></view>
<view>B. <text>{{item.answerB}}</text></view>
<view>C. <text>{{item.answerC}}</text></view>
<view>D. <text>{{item.answerD}}</text></view>
</view>
</view>
</view>
復(fù)制代碼

2.dome2,刪除了不必要的dom嵌套
<view wx:for="{{listData}}" class="first-item" wx:for-index="i" wx:for-item="firstItem" wx:key="i" wx:if="{{firstItem.isDisplay}}">
<view class="item-list" wx:for="{{firstItem.itemList}}" wx:key="index">
<view>{{item.qus}}</view>
<view class="answer-list">
<view>A. {{item.answerA}}</view>
<view>B. {{item.answerB}}</view>
<view>C. {{item.answerC}}</view>
<view>D. {{item.answerD}}</view>
</view>
</view>
</view>
復(fù)制代碼

通過大致計(jì)算,一個(gè)小程序頁面大概可以渲染2萬個(gè)wxml節(jié)點(diǎn) 而小程序官方的性能測評得分條件為少于1000個(gè)wxml節(jié)點(diǎn)[官方鏈接](https://developers.weixin.qq.com/miniprogram/dev/framework/audits/performance.html#5. setData數(shù)據(jù)大小)

二.列表頁面優(yōu)化
1.減少不必要的標(biāo)簽嵌套
由上面的測試dome可知,在不影響代碼運(yùn)行和可讀性的前提下,盡量減少標(biāo)簽的嵌套,可以大幅的增加頁面數(shù)據(jù)的列表?xiàng)l數(shù),畢竟公司不是按代碼行數(shù)發(fā)工資的。如果你的列表數(shù)據(jù)量有限,可以用這種方法來增加列表渲染條數(shù)。如果數(shù)據(jù)量很大,再怎么精簡也超過2萬的節(jié)點(diǎn),這個(gè)方法則不適用。
2.優(yōu)化setData的使用
如圖五所示,小程序setDate的性能會(huì)受到setData數(shù)據(jù)量大小和調(diào)用頻率限制。所以要圍繞減少每一次setData數(shù)據(jù)量大小,降低setData調(diào)用頻率進(jìn)行優(yōu)化。#####(1)刪除冗余字段 后端的同事經(jīng)常把數(shù)據(jù)從數(shù)據(jù)庫中取出就直接返回給前端,不經(jīng)過任何處理,所以會(huì)導(dǎo)致數(shù)據(jù)大量的冗余,很多字段根本用不到,我們需要把這些字段刪除,減少setDate的數(shù)據(jù)大小。#####(2)setData的進(jìn)階用法 通常,我們對data中數(shù)據(jù)的增刪改操作,是把原來的數(shù)據(jù)取出,處理,然后用setData整體去更新,比如我們列表中使用到的上拉加載更多,需要往listData尾部添加數(shù)據(jù):
newList=[{...},{...}];
this.setData({
listData:[...this.data.listData,...newList]
})
復(fù)制代碼
這樣會(huì)導(dǎo)致setDate的數(shù)據(jù)量越來越大,頁面也越來越卡。
setDate的正確使用姿勢
setDate修改數(shù)據(jù)
比如我們要修改數(shù)組listData第一個(gè)元素的isDisplay屬性,我們可以這樣操作:
let index=0;
this.setData({
[`listData[${index}].isDisplay`]:false,
})
復(fù)制代碼
如果我們想同時(shí)修改數(shù)組listData中下標(biāo)從0到9的元素的isDisplay屬性,那要如何處理呢?你可能會(huì)想到用for循環(huán)來執(zhí)行setData:
for(let index=0;index<10;index++){
this.setData({
[`listData[${index}].isDisplay`]:false,
})
}
那么這樣就會(huì)導(dǎo)致另外一個(gè)問題,那就是listData的調(diào)用過于頻繁,也會(huì)導(dǎo)致性能問題,正確的處理方式是先把要修改的數(shù)據(jù)先收集起來,然后調(diào)用setData一次處理完成:
let changeData={};
for(let index=0;index<10;index++){
changeData[[`listData[${index}].isDisplay`]]=false;
}
this.setData(changeData);
這樣我們就把數(shù)組listData中下標(biāo)從0到9的元素的isDisplay屬性改成了false。
setDate往數(shù)組末尾添加數(shù)據(jù)
如果只添加一條數(shù)據(jù)
let newData={...};
this.setData({
[`listData[${this.data.listData.length}]`]:newData
})
如果是添加多條數(shù)據(jù)
let newData=[{...},{...},{...},{...},{...},{...}];
let changeData={};
let index=this.data.listData.length
newData.forEach((item) => {
newData['listData[' + (index++) + ']'] = item //賦值,索引遞增
})
this.setData(changeData)
至于刪除操作,還沒有找到更好的方法,不知道大家有什么方法可以分享嗎?
三.使用自定義組件
可以把列表的一行或者多行封裝到自定義組件里,在列表頁使用一個(gè)組件,只算一個(gè)節(jié)點(diǎn),這樣你的列表能渲染的數(shù)據(jù)可以成倍數(shù)的增加。組件內(nèi)的節(jié)點(diǎn)數(shù)也是有限制的,但是你可以一層層嵌套組件實(shí)現(xiàn)列表的無限加載,如果你不怕麻煩的話
四.使用虛擬列表
經(jīng)過上面的一系列操作后,列表的性能會(huì)得到很大的提升,但是如果數(shù)據(jù)量實(shí)在太大,wxml節(jié)點(diǎn)數(shù)也會(huì)超出限制,導(dǎo)致頁面發(fā)生錯(cuò)誤。我們的處理方法是使用虛擬列表,頁面只渲染當(dāng)前可視區(qū)域以及可視區(qū)域上下若干條數(shù)據(jù)的節(jié)點(diǎn),通過isDisplay控制節(jié)點(diǎn)的渲染。
可視區(qū)域上方: above可視區(qū)域: screen可視區(qū)域下方: below

1.listData數(shù)組的結(jié)構(gòu)
使用二維數(shù)組,因?yàn)槿绻且痪S數(shù)組,頁面滾動(dòng)需要用setData設(shè)置大量的元素isDispaly屬性來控制列表的的渲染。而二維數(shù)組可以這可以一次調(diào)用setData控制十條,二十條甚至更多的數(shù)據(jù)的渲染。
listData:[
{
isDisplay:true,
itemList:[{
qus:'下面哪位是劉發(fā)財(cái)女朋友?',
answerA:'劉亦菲',
answerB:'迪麗熱巴',
answerC:'齋藤飛鳥',
answerD:'花澤香菜',
}
.......//二維數(shù)組中的條數(shù)根據(jù)項(xiàng)目實(shí)際情況
]
}]
2.必要的參數(shù)
data{
itemHeight:4520,//列表第一層dom高度,單位為rpx
itemPxHeight:'',//轉(zhuǎn)化為px高度,因?yàn)樾〕绦颢@取的滾動(dòng)條高度單位為px
aboveShowIndex:0,//已渲染數(shù)據(jù)的第一條的Index
belowShowNum:0,//顯示區(qū)域下方隱藏的條數(shù)
oldSrollTop:0,//記錄上一次滾動(dòng)的滾動(dòng)條高度,判斷滾動(dòng)方向
prepareNum:5,//可視區(qū)域上下方要渲染的數(shù)量
throttleTime:200,//滾動(dòng)事件節(jié)流的時(shí)間,單位ms
}
3.wxml的dom結(jié)構(gòu)
<!-- above區(qū)域的 -->
<view class="above-box" style="height:{{aboveShowIndex*itemHeight}}rpx"> </view>
<!-- 實(shí)際渲染的區(qū)域的 -->
<view wx:for="{{listData}}" class="first-item" wx:for-index="i" wx:for-item="firstItem" wx:key="i" wx:if="{{firstItem.isDisplay}}">
<view class="item-list" wx:for="{{firstItem.itemList}}" wx:key="index">
<view>{{item.qus}}</view>
<view class="answer-list">
<view>A. {{item.answerA}}</view>
<view>B. {{item.answerB}}</view>
<view>C. {{item.answerC}}</view>
<view>D. {{item.answerD}}</view>
</view>
</view>
</view>
<!-- below區(qū)域的 -->
<view class="below-box" style="height:{{belowShowNum*itemHeight}}rpx"> </view>
4.獲取列表第一層dom的px高度
let query = wx.createSelectorQuery();
query.select('.content').boundingClientRect(rect=>{
let clientWidth = rect.width;
let ratio = 750 / clientWidth;
this.setData({
itemPxHeight:Math.floor(this.data.itemHeight/ratio),
})
}).exec();
5.頁面滾動(dòng)時(shí)間節(jié)流
function throttle(fn){
let valid = true
return function() {
if(!valid){
return false
}
// 工作時(shí)間,執(zhí)行函數(shù)并且在間隔期內(nèi)把狀態(tài)位設(shè)為無效
valid = false
setTimeout(() => {
fn.call(this,arguments);
valid = true;
}, this.data.throttleTime)
}
}
6.頁面滾動(dòng)事件處理
onPageScroll:throttle(function(e){
let scrollTop=e[0].scrollTop;//滾動(dòng)條高度
let itemNum=Math.floor(scrollTop/this.data.itemPxHeight);//計(jì)算出可視區(qū)域的數(shù)據(jù)Index
let clearindex=itemNum-this.data.prepareNum+1;//滑動(dòng)后需要渲染數(shù)據(jù)第一條的index
let oldSrollTop=this.data.oldSrollTop;//滾動(dòng)前的scrotop,用于判斷滾動(dòng)的方向
let aboveShowIndex=this.data.aboveShowIndex;//獲取已渲染數(shù)據(jù)第一條的index
let listDataLen=this.data.listData.length;
let changeData={}
//向下滾動(dòng)
if(scrollTop-oldSrollTop>0){
if(clearindex>0){
//滾動(dòng)后需要變更的條數(shù)
for(let i=aboveShowIndex;i<clearindex;i++){
changeData[[`listData[${i}].isDisplay`]]=false;
let belowShowIndex=i+2*this.data.prepareNum;
if(i+2*this.data.prepareNum<listDataLen){
changeData[[`listData[${belowShowIndex}].isDisplay`]]=true;
}
}
}
}else{//向上滾動(dòng)
if(clearindex>=0){
let changeData={}
for(let i=aboveShowIndex-1;i>=clearindex;i--){
let belowShowIndex=i+2*this.data.prepareNum
if(i+2*this.data.prepareNum<=listDataLen-1){
changeData[[`listData[${belowShowIndex}].isDisplay`]]=false;
}
changeData[[`listData[${i}].isDisplay`]]=true;
}
}else{
if(aboveShowIndex>0){
for(let i=0;i<aboveShowIndex;i++){
this.setData({
[`listData[${i}].isDisplay`]:true,
})
}
}
}
}
clearindex=clearindex>0?clearindex:0
if(clearindex>=0&&!(clearindex>0&&clearindex==this.data.aboveShowIndex)){
changeData.aboveShowIndex=clearindex;
let belowShowNum=this.data.listData.length-(2*this.data.prepareNum+clearindex)
belowShowNum=belowShowNum>0?belowShowNum:0
if(belowShowNum>=0){
changeData.belowShowNum=belowShowNum
}
this.setData(changeData)
}
this.setData({
oldSrollTop:scrollTop
})
}),
經(jīng)過上面的處理后,頁面的wxml節(jié)點(diǎn)數(shù)量相對穩(wěn)定,可能因?yàn)榭梢晠^(qū)域數(shù)據(jù)的index計(jì)算誤差,頁面渲染的數(shù)據(jù)有小幅度的浮動(dòng),但是已經(jīng)完全不會(huì)超過小程序頁面的節(jié)點(diǎn)數(shù)量的限制。理論上100萬條數(shù)據(jù)的列表也不會(huì)有問題,只要你有耐心和精力一直劃列表加載這么多數(shù)據(jù)。
7.待優(yōu)化事項(xiàng)
列表每一行的高度需要固定,不然會(huì)導(dǎo)致可視區(qū)域數(shù)據(jù)的index的計(jì)算出現(xiàn)誤差 渲染玩列表后往回來列表,如果手速過快,會(huì)導(dǎo)致above,below區(qū)域的數(shù)據(jù)渲染不過來,會(huì)出現(xiàn)短暫的白屏,白屏問題可以調(diào)整 prepareNum,throttleTime兩個(gè)參數(shù)改善,但是不能完全解決。如果列表中有圖片,above,below區(qū)域重新渲染時(shí),圖片雖然以經(jīng)緩存在本地,不需要重新去服務(wù)器請求,但是重新渲染還是需要時(shí)間,尤其當(dāng)你手速特別快時(shí)。可以根據(jù)上面的思路, isDisplay時(shí)只銷毀非<image>的節(jié)點(diǎn),這樣重新渲染就不需要渲染圖片,但是這樣節(jié)點(diǎn)數(shù)還是會(huì)增加,不過應(yīng)該能滿足大部分項(xiàng)目需求了,看自己項(xiàng)目怎么取舍。
五.使用自定義組件和虛擬列表的對比。
雖然不知道為什么,但是直覺告訴我使用自定義組件性能會(huì)相對差一點(diǎn)。為了對比兩種方法的優(yōu)劣,使用了Trace工具對一個(gè)5000條帶圖片數(shù)據(jù)進(jìn)行了性能測試。
內(nèi)存占用對比:
自定義組件內(nèi)存占用情況:

虛擬列表內(nèi)存占用情況:

對比可以看出,因?yàn)榻M件在上拉加載時(shí),組件是沒有銷毀的,導(dǎo)致數(shù)據(jù)量逐漸增多。而虛擬列表在增加數(shù)據(jù)的同時(shí),也會(huì)銷毀相同數(shù)量的數(shù)據(jù),所以內(nèi)存占比會(huì)穩(wěn)定在一個(gè)數(shù)量。具體到這個(gè)測試dome,5000條數(shù)據(jù)使用自定義組件,最后占用2000MB的內(nèi)存,而虛擬列表穩(wěn)定在700MB。
setData后重新渲染所用的時(shí)間對比:
自定義組件重新渲染耗時(shí):

虛擬列表重新渲染耗時(shí):

從測試結(jié)果可以看出,無論是耗時(shí)的次數(shù)分布,還是最大耗時(shí),最小耗時(shí),虛擬列表都優(yōu)于自定義組件
最后附上虛擬列表的github地址,如果對您有幫助,記得給個(gè)小星星哦
https://github.com/lmn1919/wechatApp-dome/tree/main/pages/list-scroll-view
The End
歡迎自薦投稿到《前端技術(shù)江湖》,如果你覺得這篇內(nèi)容對你挺有啟發(fā),記得點(diǎn)個(gè) 「在看」哦
點(diǎn)個(gè)『在看』支持下 
