某天閑著無聊想練一下手速,去上拉一個小程序項目中一個有1萬多條商品數據的列表。在數據加載到1000多條后,是列表居然出現了白屏。看了一下控制臺:
圖一
‘Dom limit ’,dom數超出了限制, 不知道微信是出于什么考慮,要限制頁面的dom數量。
一.小程序頁面限制多少個wxml節點?
寫了個小dome做了個測試。的數據結構為:
listData:[
???{
????isDisplay:true,
????itemList:[{
??????????qus:'下面哪位是劉發財女朋友?',
??????????answerA:'劉亦菲',
??????????answerB:'迪麗熱巴',
??????????answerC:'齋藤飛鳥',
??????????answerD:'花澤香菜',
???????}
??????.......//20條數據
?????]
???}]
頁面渲染效果:
圖二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>
復制代碼
圖三 運行結果:渲染了72*20條數據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>
復制代碼
圖四 運行結果:渲染了113*20條數據
通過大致計算,一個小程序頁面大概可以渲染2萬個wxml節點而小程序官方的性能測評得分條件為少于1000個wxml節點[官方鏈接](#5. 數據大小)
圖五 小程序性能評分二.列表頁面優化1.減少不必要的標簽嵌套
由上面的測試dome可知,在不影響代碼運行和可讀性的前提下,盡量減少標簽的嵌套,可以大幅的增加頁面數據的列表條數,畢竟公司不是按代碼行數發工資的。如果你的列表數據量有限,可以用這種方法來增加列表渲染條數。如果數據量很大,再怎么精簡也超過2萬的節點,這個方法則不適用。
2.優化的使用
如圖五所示,小程序的性能會受到數據量大小和調用頻率限制。所以要圍繞減少每一次數據量大小,降低調用頻率進行優化。#####(1)刪除冗余字段 后端的同事經常把數據從數據庫中取出就直接返回給前端,不經過任何處理,所以會導致數據大量的冗余,很多字段根本用不到,我們需要把這些字段刪除,減少的數據大小。#####(2)的進階用法 通常,我們對data中數據的增刪改操作,是把原來的數據取出,處理,然后用整體去更新,比如我們列表中使用到的上拉加載更多,需要往尾部添加數據:
????newList=[{...},{...}];
???this.setData({
?????listData:[...this.data.listData,...newList]
???})
復制代碼
這樣會導致的數據量越來越大,頁面也越來越卡。
的正確使用姿勢
比如我們要修改數組第一個元素的屬性,我們可以這樣操作:
??let?index=0;
??this.setData({
?????[`listData[${index}].isDisplay`]:false,
??})
復制代碼
如果我們想同時修改數組中下標從0到9的元素的屬性,那要如何處理呢?你可能會想到用for循環來執行:
??for(let?index=0;index<10;index++){
?????this.setData({
????????[`listData[${index}].isDisplay`]:false,
?????})
??}
那么這樣就會導致另外一個問題,那就是的調用過于頻繁,也會導致性能問題,正確的處理方式是先把要修改的數據先收集起來,然后調用一次處理完成:
??let?changeData={};
??for(let?index=0;index<10;index++){
??????changeData[[`listData[${index}].isDisplay`]]=false;
??}
??this.setData(changeData);
這樣我們就把數組中下標從0到9的元素的屬性改成了false。
如果只添加一條數據
??let?newData={...};
??this.setData({
????[`listData[${this.data.listData.length}]`]:newData
??})
如果是添加多條數據
??let?newData=[{...},{...},{...},{...},{...},{...}];
??let?changeData={};
??let?index=this.data.listData.length
????newData.forEach((item)?=>?{
????????newData['listData['?+?(index++)?+?']']?=?item?//賦值,索引遞增
????})?
??this.setData(changeData)
至于刪除操作,還沒有找到更好的方法,不知道大家有什么方法可以分享嗎?
三.使用自定義組件
可以把列表的一行或者多行封裝到自定義組件里,在列表頁使用一個組件,只算一個節點,這樣你的列表能渲染的數據可以成倍數的增加。組件內的節點數也是有限制的,但是你可以一層層嵌套組件實現列表的無限加載,如果你不怕麻煩的話
四.使用虛擬列表
經過上面的一系列操作后,列表的性能會得到很大的提升,但是如果數據量實在太大,wxml節點數也會超出限制,導致頁面發生錯誤。我們的處理方法是使用虛擬列表,頁面只渲染當前可視區域以及可視區域上下若干條數據的節點,通過控制節點的渲染。
圖六 節點渲染示意圖1.數組的結構
使用二維數組,因為如果是一維數組,頁面滾動需要用設置大量的元素屬性來控制列表的的渲染。而二維數組可以這可以一次調用控制十條,二十條甚至更多的數據的渲染。
listData:[
???{
????isDisplay:true,
????itemList:[{
??????????qus:'下面哪位是劉發財女朋友?',
??????????answerA:'劉亦菲',
??????????answerB:'迪麗熱巴',
??????????answerC:'齋藤飛鳥',
??????????answerD:'花澤香菜',
???????}
??????.......//二維數組中的條數根據項目實際情況
?????]
???}]
2.必要的參數
???data{
???????itemHeight:4520,//列表第一層dom高度,單位為rpx
???????itemPxHeight:'',//轉化為px高度,因為小程序獲取的滾動條高度單位為px
???????aboveShowIndex:0,//已渲染數據的第一條的Index
???????belowShowNum:0,//顯示區域下方隱藏的條數
???????oldSrollTop:0,//記錄上一次滾動的滾動條高度,判斷滾動方向
???????prepareNum:5,//可視區域上下方要渲染的數量
???????throttleTime:200,//滾動事件節流的時間,單位ms
???}
3.wxml的dom結構
????
????<view?class="above-box"?style="height:{{aboveShowIndex*itemHeight}}rpx">?view>
???
????<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>
????
????<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.頁面滾動時間節流
function?throttle(fn){
??let?valid?=?true
??return?function()?{
?????if(!valid){
?????????return?false?
?????}
?????//?工作時間,執行函數并且在間隔期內把狀態位設為無效
??????valid?=?false
??????setTimeout(()?=>?{
??????????fn.call(this,arguments);
??????????valid?=?true;
??????},?this.data.throttleTime)
??}
}
6.頁面滾動事件處理
???onPageScroll:throttle(function(e){
????let?scrollTop=e[0].scrollTop;//滾動條高度
????let?itemNum=Math.floor(scrollTop/this.data.itemPxHeight);//計算出可視區域的數據Index
????let?clearindex=itemNum-this.data.prepareNum+1;//滑動后需要渲染數據第一條的index
????let?oldSrollTop=this.data.oldSrollTop;//滾動前的scrotop,用于判斷滾動的方向
????let?aboveShowIndex=this.data.aboveShowIndex;//獲取已渲染數據第一條的index
????let?listDataLen=this.data.listData.length;
????let?changeData={}
??//向下滾動
????if(scrollTop-oldSrollTop>0){
????????if(clearindex>0){
?????????//滾動后需要變更的條數
??????????for(let?i=aboveShowIndex;i????????????????changeData[[`listData[${i}].isDisplay`]]=false;
????????????????let?belowShowIndex=i+2*this.data.prepareNum;
????????????????if(i+2*this.data.prepareNum??????????????????changeData[[`listData[${belowShowIndex}].isDisplay`]]=true;
?????????????????}
??????????}???
????????}????
????}else{//向上滾動
????????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??????????????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
????})
??}),
經過上面的處理后,頁面的wxml節點數量相對穩定,可能因為可視區域數據的index計算誤差,頁面渲染的數據有小幅度的浮動,但是已經完全不會超過小程序頁面的節點數量的限制。理論上100萬條數據的列表也不會有問題,只要你有耐心和精力一直劃列表加載這么多數據。
7.待優化事項五.使用自定義組件和虛擬列表的對比。
雖然不知道為什么,但是直覺告訴我使用自定義組件性能會相對差一點。為了對比兩種方法的優劣,使用了Trace工具對一個5000條帶圖片數據進行了性能測試。
內存占用對比:
自定義組件內存占用情況:
圖七 自定義組件內存占用情況
虛擬列表內存占用情況:
圖八 虛擬列表內存占用情況
對比可以看出,因為組件在上拉加載時,組件是沒有銷毀的小程序帶參數分享,導致數據量逐漸增多。而虛擬列表在增加數據的同時,也會銷毀相同數量的數據,所以內存占比會穩定在一個數量。具體到這個測試dome,5000條數據使用自定義組件,最后占用的內存,而虛擬列表穩定在700MB。
后重新渲染所用的時間對比:
自定義組件重新渲染耗時:
圖九 自定義組件重新渲染耗時
虛擬列表重新渲染耗時:
圖十 虛擬列表重新渲染耗時
從測試結果可以看出,無論是耗時的次數分布,還是最大耗時,最小耗時,虛擬列表都優于自定義組件
最后附上虛擬列表的地址,如果對您有幫助,記得給個小星星哦
如果覺得這篇文章還不錯點擊下面卡片關注我來個【分享、點贊、在看】三連支持一下吧