小程序长列表性能优化实践

前端壹栈

共 15868字,需浏览 32分钟

 ·

2021-06-06 12:54


作者:lmq1919

https://juejin.cn/post/6966904317148299271


某天闲着无聊想练一下手速,去上拉一个小程序项目中一个有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节点[官方链接](https://developers.weixin.qq.com/miniprogram/dev/framework/audits/performance.html#5. setData数据大小)

图五  小程序性能评分

二.列表页面优化

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) => {
        newData['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地址,如果对您有帮助,记得给个小星星哦

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

The End

欢迎自荐投稿到《前端技术江湖》,如果你觉得这篇内容对你挺有启发,记得点个 「在看」


点个『在看』支持下 

浏览 55
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报