1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        小程序長(zhǎng)列表性能優(yōu)化實(shí)踐

        共 15896字,需瀏覽 32分鐘

         ·

        2021-06-05 18:00

        點(diǎn)擊上方關(guān)注 前端技術(shù)江湖一起學(xué)習(xí),天天進(jìn)步

        作者:lmq1919

        https://juejin.cn/post/6966904317148299271


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

        圖一

        ‘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>
        復(fù)制代碼

        圖三  運(yùn)行結(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ù)制代碼

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

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

        圖五  小程序性能評(píng)分

        二.列表頁(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]
           })
        復(fù)制代碼

        這樣會(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,
          })
        復(fù)制代碼

        如果我們想同時(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.length
            newData.forEach((item) => {
                newData['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
        圖六  節(jié)點(diǎn)渲染示意圖

        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高度,單位為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.頁(yè)面滾動(dòng)時(shí)間節(jié)流

        function throttle(fn){
          let valid = true
          return function({
             if(!valid){
                 return false 
             }
             // 工作時(shí)間,執(zhí)行函數(shù)并且在間隔期內(nèi)把狀態(tài)位設(shè)為無(wú)效
              valid = false
              setTimeout(() => {
                  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ù)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)過(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ù)改善,但是不能完全解決。
        • 如果列表中有圖片,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)該能滿足大部分項(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)存占用情況

        虛擬列表內(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ù),所以內(nèi)存占比會(huì)穩(wěn)定在一個(gè)數(shù)量。具體到這個(gè)測(cè)試dome,5000條數(shù)據(jù)使用自定義組件,最后占用2000MB的內(nèi)存,而虛擬列表穩(wěn)定在700MB。

        setData后重新渲染所用的時(shí)間對(duì)比:

        自定義組件重新渲染耗時(shí):

        圖九   自定義組件重新渲染耗時(shí)

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

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

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

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

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

        The End

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


        點(diǎn)個(gè)『在看』支持下 

        瀏覽 33
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            探花网址在线观看 | 婷婷激情综合色五月久久竹菊影视 | 刘亦菲张开双腿流白 | 免费黄视频网站 | 国产亚洲 久一区二区 | 男女羞羞羞 | theav精尽人亡av | 欧美大屌在线观看 | 爱逼综合 | 自拍偷拍视频一区二区 |