如何讓10萬(wàn)條數(shù)據(jù)的小程序列表如絲般順滑

作者 | 劉發(fā)福
鏈接 | https://juejin.cn/post/6966904317148299271

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

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>

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>

通過(guò)大致計(jì)算,一個(gè)小程序頁(yè)面大概可以渲染2萬(wàn)個(gè)wxml節(jié)點(diǎn) 而小程序官方的性能測(cè)評(píng)得分條件為少于1000個(gè)wxml節(jié)點(diǎn)官方鏈接
4、 setData調(diào)用頻率
setData接口的調(diào)用涉及邏輯層與渲染層間的線程通信,通信過(guò)于頻繁可能導(dǎo)致處理隊(duì)列阻塞,界面渲染不及時(shí)而導(dǎo)致卡頓,應(yīng)避免無(wú)用的頻繁調(diào)用。
得分條件:每秒調(diào)用setData的次數(shù)不超過(guò) 20 次
5、 setData數(shù)據(jù)大小
由于小程序運(yùn)行邏輯線程與渲染線程之上,setData的調(diào)用會(huì)把數(shù)據(jù)從邏輯層傳到渲染層,數(shù)據(jù)太大會(huì)增加通信時(shí)間。
得分條件:setData的數(shù)據(jù)在jsON.stringify后不超過(guò) 256KB
6、 WXML節(jié)點(diǎn)數(shù)
建議一個(gè)頁(yè)面使用少于 1000 個(gè) WXML 節(jié)點(diǎn),節(jié)點(diǎn)樹(shù)深度少于 30 層,子節(jié)點(diǎn)數(shù)不大于 60 個(gè)。一個(gè)太大的 WXML 節(jié)點(diǎn)樹(shù)會(huì)增加內(nèi)存的使用,樣式重排時(shí)間也會(huì)更長(zhǎng),影響體驗(yàn)。
得分條件:頁(yè)面WXML節(jié)點(diǎn)少于 1000 個(gè),節(jié)點(diǎn)樹(shù)深度少于 30 層,子節(jié)點(diǎn)數(shù)不大于 60 個(gè)。
二、列表頁(yè)面優(yōu)化
1、減少不必要的標(biāo)簽嵌套
由上面的測(cè)試dome可知,在不影響代碼運(yùn)行和可讀性的前提下,盡量減少標(biāo)簽的嵌套,可以大幅的增加頁(yè)面數(shù)據(jù)的列表?xiàng)l數(shù),畢竟公司不是按代碼行數(shù)發(fā)工資的。如果你的列表數(shù)據(jù)量有限,可以用這種方法來(lái)增加列表渲染條數(shù)。如果數(shù)據(jù)量很大,再怎么精簡(jiǎn)也超過(guò)2萬(wàn)的節(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ù)庫(kù)中取出就直接返回給前端,不經(jīng)過(guò)任何處理,所以會(huì)導(dǎo)致數(shù)據(jù)大量的冗余,很多字段根本用不到,我們需要把這些字段刪除,減少setDate的數(shù)據(jù)大小。
(2)setData的進(jìn)階用法 通常,我們對(duì)data中數(shù)據(jù)的增刪改操作,是把原來(lái)的數(shù)據(jù)取出,處理,然后用setData整體去更新,比如我們列表中使用到的上拉加載更多,需要往listData尾部添加數(shù)據(jù):
newList=[{...},{...}];this.setData({listData:[...this.data.listData,...newList]})
這樣會(huì)導(dǎo)致 setDate的數(shù)據(jù)量越來(lái)越大,頁(yè)面也越來(lái)越卡。
setDate的正確使用姿勢(shì)
setDate修改數(shù)據(jù)
比如我們要修改數(shù)組listData第一個(gè)元素的isDisplay屬性,我們可以這樣操作:
let index=0;this.setData({[`listData[${index}].isDisplay`]:false,})
如果我們想同時(shí)修改數(shù)組listData中下標(biāo)從0到9的元素的isDisplay屬性,那要如何處理呢?你可能會(huì)想到用for循環(huán)來(lái)執(zhí)行setData:
for(let index=0;index<10;index++){this.setData({[`listData[${index}].isDisplay`]:false,})}
那么這樣就會(huì)導(dǎo)致另外一個(gè)問(wèn)題,那就是listData的調(diào)用過(guò)于頻繁,也會(huì)導(dǎo)致性能問(wèn)題,正確的處理方式是先把要修改的數(shù)據(jù)先收集起來(lái),然后調(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.lengthnewData.forEach((item) => {changeData['listData[' + (index++) + ']'] = item //賦值,索引遞增})this.setData(changeData)
至于刪除操作,還沒(méi)有找到更好的方法,不知道大家有什么方法可以分享嗎?
三、使用自定義組件
可以把列表的一行或者多行封裝到自定義組件里,在列表頁(yè)使用一個(gè)組件,只算一個(gè)節(jié)點(diǎn),這樣你的列表能渲染的數(shù)據(jù)可以成倍數(shù)的增加。
組件內(nèi)的節(jié)點(diǎn)數(shù)也是有限制的,但是你可以一層層嵌套組件實(shí)現(xiàn)列表的無(wú)限加載,如果你不怕麻煩的話
四、使用虛擬列表
經(jīng)過(guò)上面的一系列操作后,列表的性能會(huì)得到很大的提升,但是如果數(shù)據(jù)量實(shí)在太大,wxml節(jié)點(diǎn)數(shù)也會(huì)超出限制,導(dǎo)致頁(yè)面發(fā)生錯(cuò)誤。
我們的處理方法是使用虛擬列表,頁(yè)面只渲染當(dāng)前可視區(qū)域以及可視區(qū)域上下若干條數(shù)據(jù)的節(jié)點(diǎn),通過(guò)isDisplay控制節(jié)點(diǎn)的渲染。
可視區(qū)域上方:above
可視區(qū)域:screen
可視區(qū)域下方:below

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

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

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

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

從測(cè)試結(jié)果可以看出,無(wú)論是耗時(shí)的次數(shù)分布,還是最大耗時(shí),最小耗時(shí),虛擬列表都優(yōu)于自定義組件
最后附上虛擬列表的github地址:https://github.com/lmn1919/wechatApp-dome/tree/main/pages/list-scroll-view
如果對(duì)您有幫助,記得給個(gè)贊哦。
學(xué)習(xí)更多技能
請(qǐng)點(diǎn)擊下方公眾號(hào)
![]()

