7

如何让10万条数据的小程序列表如丝般顺滑

 3 years ago
source link: https://juejin.cn/post/6966904317148299271
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
2021年05月27日 阅读 5309

如何让10万条数据的小程序列表如丝般顺滑

某天闲着无聊想练一下手速,去上拉一个小程序项目中一个有1万多条商品数据的列表。在数据加载到1000多条后,是列表居然出现了白屏。看了一下控制台:

图一

‘Dom limit exceeded’,dom数超出了限制, 不知道微信是出于什么考虑,要限制页面的dom数量。

一.小程序页面限制多少个wxml节点?

写了个小dome做了个测试。 listData的数据结构为:

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节点官方链接

图五  小程序性能评分

二.列表页面优化

1.减少不必要的标签嵌套

由上面的测试dome可知,在不影响代码运行和可读性的前提下,尽量减少标签的嵌套,可以大幅的增加页面数据的列表条数,毕竟公司不是按代码行数发工资的。如果你的列表数据量有限,可以用这种方法来增加列表渲染条数。如果数据量很大,再怎么精简也超过2万的节点,这个方法则不适用。

2.优化setData的使用

图五所示,小程序setDate的性能会受到setData数据量大小和调用频率限制。所以要围绕减少每一次setData数据量大小,降低setData调用频率进行优化。 #####(1)删除冗余字段 后端的同事经常把数据从数据库中取出就直接返回给前端,不经过任何处理,所以会导致数据大量的冗余,很多字段根本用不到,我们需要把这些字段删除,减少setDate的数据大小。 #####(2)setData的进阶用法 通常,我们对data中数据的增删改操作,是把原来的数据取出,处理,然后用setData整体去更新,比如我们列表中使用到的上拉加载更多,需要往listData尾部添加数据:

    newList=[{...},{...}];
   this.setData({
     listData:[...this.data.listData,...newList]
   })
复制代码

这样会导致 setDate的数据量越来越大,页面也越来越卡。

setDate的正确使用姿势

  • setDate修改数据

比如我们要修改数组listData第一个元素的isDisplay属性,我们可以这样操作:

  let index=0;
  this.setData({
     [`listData[${index}].isDisplay`]:false,
  })
复制代码

如果我们想同时修改数组listData中下标从0到9的元素的isDisplay属性,那要如何处理呢?你可能会想到用for循环来执行setData

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

那么这样就会导致另外一个问题,那就是listData的调用过于频繁,也会导致性能问题,正确的处理方式是先把要修改的数据先收集起来,然后调用setData一次处理完成:

  let changeData={};
  for(let index=0;index<10;index++){
      changeData[[`listData[${index}].isDisplay`]]=false;
  }
  this.setData(changeData);
复制代码

这样我们就把数组listData中下标从0到9的元素的isDisplay属性改成了false

  • setDate往数组末尾添加数据

如果只添加一条数据

  let newData={...};
  this.setData({
    [`listData[${this.data.listData.length}]`]:newData
  })
复制代码

如果是添加多条数据

  let newData=[{...},{...},{...},{...},{...},{...}];
  let changeData={};
  let index=this.data.listData.length
    newData.forEach((item) => {
        changeData['listData[' + (index++) + ']'] = item //赋值,索引递增
    }) 
  this.setData(changeData)
复制代码

至于删除操作,还没有找到更好的方法,不知道大家有什么方法可以分享吗?

三.使用自定义组件

可以把列表的一行或者多行封装到自定义组件里,在列表页使用一个组件,只算一个节点,这样你的列表能渲染的数据可以成倍数的增加。组件内的节点数也是有限制的,但是你可以一层层嵌套组件实现列表的无限加载,如果你不怕麻烦的话

四.使用虚拟列表

经过上面的一系列操作后,列表的性能会得到很大的提升,但是如果数据量实在太大,wxml节点数也会超出限制,导致页面发生错误。我们的处理方法是使用虚拟列表,页面只渲染当前可视区域以及可视区域上下若干条数据的节点,通过isDisplay控制节点的渲染。

  • 可视区域上方:above
  • 可视区域:screen
  • 可视区域下方:below

1.listData数组的结构

使用二维数组,因为如果是一维数组,页面滚动需要用setData设置大量的元素isDispaly属性来控制列表的的渲染。而二维数组可以这可以一次调用setData控制十条,二十条甚至更多的数据的渲染。

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结构

    <!-- above区域的 -->
    <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>
    <!-- below区域的 -->
    <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<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
    })
  }),
复制代码

经过上面的处理后,页面的wxml节点数量相对稳定,可能因为可视区域数据的index计算误差,页面渲染的数据有小幅度的浮动,但是已经完全不会超过小程序页面的节点数量的限制。理论上100万条数据的列表也不会有问题,只要你有耐心和精力一直划列表加载这么多数据。

7.待优化事项

  • 列表每一行的高度需要固定,不然会导致可视区域数据的index的计算出现误差
  • 渲染玩列表后往回来列表,如果手速过快,会导致above,below区域的数据渲染不过来,会出现短暂的白屏,白屏问题可以调整 prepareNum, throttleTime两个参数改善,但是不能完全解决(经过测试对比发现,即使不对列表进行任何处理,滑动速度过快也会发生短暂白屏的情况)。
  • 如果列表中有图片,above,below区域重新渲染时,图片虽然以经缓存在本地,不需要重新去服务器请求,但是重新渲染还是需要时间,尤其当你手速特别快时。可以根据上面的思路, isDisplay时只销毁非<image>的节点,这样重新渲染就不需要渲染图片,但是这样节点数还是会增加,不过应该能满足大部分项目需求了,看自己项目怎么取舍。

五.使用自定义组件和虚拟列表的对比。

虽然不知道为什么,但是直觉告诉我使用自定义组件性能会相对差一点。为了对比两种方法的优劣,使用了Trace工具对一个5000条带图片数据进行了性能测试。

内存占用对比:

自定义组件内存占用情况:

虚拟列表内存占用情况:

对比可以看出,因为组件在上拉加载时,组件是没有销毁的,导致数据量逐渐增多。而虚拟列表在增加数据的同时,也会销毁相同数量的数据,所以内存占比会稳定在一个数量。具体到这个测试dome,5000条数据使用自定义组件,最后占用2000MB的内存,而虚拟列表稳定在700MB。

setData后重新渲染所用的时间对比:

自定义组件重新渲染耗时:

虚拟列表重新渲染耗时:

从测试结果可以看出,无论是耗时的次数分布,还是最大耗时,最小耗时,虚拟列表都优于自定义组件

最后附上虚拟列表的github地址,如果对您有帮助,记得给个小星星哦

github


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK