<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          小程序長列表性能優(yōu)化實踐

          共 15868字,需瀏覽 32分鐘

           ·

          2021-06-06 12:54


          作者:lmq1919

          https://juejin.cn/post/6966904317148299271


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

          圖一

          ‘Dom limit exceeded’,dom數(shù)超出了限制, 不知道微信是出于什么考慮,要限制頁面的dom數(shù)量。

          一.小程序頁面限制多少個wxml節(jié)點?

          寫了個小dome做了個測試。listData的數(shù)據(jù)結(jié)構(gòu)為:

          listData:[
             {
              isDisplay:true,
              itemList:[{
                    qus:'下面哪位是劉發(fā)財女朋友?',
                    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ù)制代碼

          圖三  運行結(jié)果:渲染了72*20條數(shù)據(jù)

          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ù)制代碼

          圖四   運行結(jié)果:渲染了113*20條數(shù)據(jù)

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

          圖五  小程序性能評分

          二.列表頁面優(yōu)化

          1.減少不必要的標(biāo)簽嵌套

          由上面的測試dome可知,在不影響代碼運行和可讀性的前提下,盡量減少標(biāo)簽的嵌套,可以大幅的增加頁面數(shù)據(jù)的列表條數(shù),畢竟公司不是按代碼行數(shù)發(fā)工資的。如果你的列表數(shù)據(jù)量有限,可以用這種方法來增加列表渲染條數(shù)。如果數(shù)據(jù)量很大,再怎么精簡也超過2萬的節(jié)點,這個方法則不適用。

          2.優(yōu)化setData的使用

          圖五所示,小程序setDate的性能會受到setData數(shù)據(jù)量大小和調(diào)用頻率限制。所以要圍繞減少每一次setData數(shù)據(jù)量大小,降低setData調(diào)用頻率進(jìn)行優(yōu)化。#####(1)刪除冗余字段 后端的同事經(jīng)常把數(shù)據(jù)從數(shù)據(jù)庫中取出就直接返回給前端,不經(jīng)過任何處理,所以會導(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ù)制代碼

          這樣會導(dǎo)致setDate的數(shù)據(jù)量越來越大,頁面也越來越卡。

          setDate的正確使用姿勢

          • setDate修改數(shù)據(jù)

          比如我們要修改數(shù)組listData第一個元素的isDisplay屬性,我們可以這樣操作:

            let index=0;
            this.setData({
               [`listData[${index}].isDisplay`]:false,
            })
          復(fù)制代碼

          如果我們想同時修改數(shù)組listData中下標(biāo)從0到9的元素的isDisplay屬性,那要如何處理呢?你可能會想到用for循環(huán)來執(zhí)行setData

            for(let index=0;index<10;index++){
               this.setData({
                  [`listData[${index}].isDisplay`]:false,
               })
            }

          那么這樣就會導(dǎo)致另外一個問題,那就是listData的調(diào)用過于頻繁,也會導(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)

          至于刪除操作,還沒有找到更好的方法,不知道大家有什么方法可以分享嗎?

          三.使用自定義組件

          可以把列表的一行或者多行封裝到自定義組件里,在列表頁使用一個組件,只算一個節(jié)點,這樣你的列表能渲染的數(shù)據(jù)可以成倍數(shù)的增加。組件內(nèi)的節(jié)點數(shù)也是有限制的,但是你可以一層層嵌套組件實現(xiàn)列表的無限加載,如果你不怕麻煩的話

          四.使用虛擬列表

          經(jīng)過上面的一系列操作后,列表的性能會得到很大的提升,但是如果數(shù)據(jù)量實在太大,wxml節(jié)點數(shù)也會超出限制,導(dǎo)致頁面發(fā)生錯誤。我們的處理方法是使用虛擬列表,頁面只渲染當(dāng)前可視區(qū)域以及可視區(qū)域上下若干條數(shù)據(jù)的節(jié)點,通過isDisplay控制節(jié)點的渲染。

          • 可視區(qū)域上方:above
          • 可視區(qū)域:screen
          • 可視區(qū)域下方:below

          圖六  節(jié)點渲染示意圖

          1.listData數(shù)組的結(jié)構(gòu)

          使用二維數(shù)組,因為如果是一維數(shù)組,頁面滾動需要用setData設(shè)置大量的元素isDispaly屬性來控制列表的的渲染。而二維數(shù)組可以這可以一次調(diào)用setData控制十條,二十條甚至更多的數(shù)據(jù)的渲染。

          listData:[
             {
              isDisplay:true,
              itemList:[{
                    qus:'下面哪位是劉發(fā)財女朋友?',
                    answerA:'劉亦菲',
                    answerB:'迪麗熱巴',
                    answerC:'齋藤飛鳥',
                    answerD:'花澤香菜',
                 }
                .......//二維數(shù)組中的條數(shù)根據(jù)項目實際情況
               ]
             }]

          2.必要的參數(shù)

             data{
                 itemHeight:4520,//列表第一層dom高度,單位為rpx
                 itemPxHeight:'',//轉(zhuǎn)化為px高度,因為小程序獲取的滾動條高度單位為px
                 aboveShowIndex:0,//已渲染數(shù)據(jù)的第一條的Index
                 belowShowNum:0,//顯示區(qū)域下方隱藏的條數(shù)
                 oldSrollTop:0,//記錄上一次滾動的滾動條高度,判斷滾動方向
                 prepareNum:5,//可視區(qū)域上下方要渲染的數(shù)量
                 throttleTime:200//滾動事件節(jié)流的時間,單位ms
             }

          3.wxml的dom結(jié)構(gòu)

              <!-- above區(qū)域的 -->
              <view class="above-box" style="height:{{aboveShowIndex*itemHeight}}rpx"> </view>
             <!-- 實際渲染的區(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.頁面滾動時間節(jié)流

          function throttle(fn){
            let valid = true
            return function({
               if(!valid){
                   return false 
               }
               // 工作時間,執(zhí)行函數(shù)并且在間隔期內(nèi)把狀態(tài)位設(shè)為無效
                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);//計算出可視區(qū)域的數(shù)據(jù)Index
              let clearindex=itemNum-this.data.prepareNum+1;//滑動后需要渲染數(shù)據(jù)第一條的index
              let oldSrollTop=this.data.oldSrollTop;//滾動前的scrotop,用于判斷滾動的方向
              let aboveShowIndex=this.data.aboveShowIndex;//獲取已渲染數(shù)據(jù)第一條的index
              let listDataLen=this.data.listData.length;
              let changeData={}
            //向下滾動
              if(scrollTop-oldSrollTop>0){
                  if(clearindex>0){
                   //滾動后需要變更的條數(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{//向上滾動
                  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é)點數(shù)量相對穩(wěn)定,可能因為可視區(qū)域數(shù)據(jù)的index計算誤差,頁面渲染的數(shù)據(jù)有小幅度的浮動,但是已經(jīng)完全不會超過小程序頁面的節(jié)點數(shù)量的限制。理論上100萬條數(shù)據(jù)的列表也不會有問題,只要你有耐心和精力一直劃列表加載這么多數(shù)據(jù)。

          7.待優(yōu)化事項

          • 列表每一行的高度需要固定,不然會導(dǎo)致可視區(qū)域數(shù)據(jù)的index的計算出現(xiàn)誤差
          • 渲染玩列表后往回來列表,如果手速過快,會導(dǎo)致above,below區(qū)域的數(shù)據(jù)渲染不過來,會出現(xiàn)短暫的白屏,白屏問題可以調(diào)整 prepareNum,throttleTime兩個參數(shù)改善,但是不能完全解決。
          • 如果列表中有圖片,above,below區(qū)域重新渲染時,圖片雖然以經(jīng)緩存在本地,不需要重新去服務(wù)器請求,但是重新渲染還是需要時間,尤其當(dāng)你手速特別快時。可以根據(jù)上面的思路,  isDisplay時只銷毀非<image>的節(jié)點,這樣重新渲染就不需要渲染圖片,但是這樣節(jié)點數(shù)還是會增加,不過應(yīng)該能滿足大部分項目需求了,看自己項目怎么取舍。

          五.使用自定義組件和虛擬列表的對比。

          雖然不知道為什么,但是直覺告訴我使用自定義組件性能會相對差一點。為了對比兩種方法的優(yōu)劣,使用了Trace工具對一個5000條帶圖片數(shù)據(jù)進(jìn)行了性能測試。

          內(nèi)存占用對比:

          自定義組件內(nèi)存占用情況:

          圖七   自定義組件內(nèi)存占用情況

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

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

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

          setData后重新渲染所用的時間對比:

          自定義組件重新渲染耗時:

          圖九   自定義組件重新渲染耗時

          虛擬列表重新渲染耗時:

          圖十   虛擬列表重新渲染耗時

          從測試結(jié)果可以看出,無論是耗時的次數(shù)分布,還是最大耗時,最小耗時,虛擬列表都優(yōu)于自定義組件

          最后附上虛擬列表的github地址,如果對您有幫助,記得給個小星星哦

          https://github.com/lmn1919/wechatApp-dome/tree/main/pages/list-scroll-view

          The End

          歡迎自薦投稿到《前端技術(shù)江湖》,如果你覺得這篇內(nèi)容對你挺有啟發(fā),記得點個 「在看」


          點個『在看』支持下 

          瀏覽 65
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产极品久久久久久久久久 | 69精品又硬又爽又粗少妇 | 国内一区二区精品 | 国产黄片精品 | 外国操逼视频网站 |