Как сделать список апплета из 100 000 единиц данных гладким и шелковистым

Апплет WeChat

Однажды мне стало скучно, и я захотел попрактиковаться в скорости рук, поэтому я создал список из более чем 10 000 данных о продуктах в небольшом программном проекте. После того, как данные загружены в более чем 1000 элементов, это список, который фактически имеет белый экран. Взгляните на консоль:

图一

«Превышен лимит DOM», количество DOM превышает лимит. Я не знаю, почему WeChat думает об ограничении количества DOM на странице.

1. Сколько мини-страниц программы ограниченоwxmlузел?

Написал небольшой купол, чтобы сделать тест. Структура данных 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条数据

По грубым подсчетам, одна страница апплета может отображать около 20 000 страниц.wxmlузелОфициальная оценка производительности апплета меньше 1000.wxmlузелОфициальная ссылка

图五  小程序性能评分

2. Оптимизация страницы списка

1. Уменьшите количество ненужных тегов

Из вышеприведенного тестового купола видно, что при условии, что это не повлияет на выполнение и читабельность кода, минимизация вложенности тегов может значительно увеличить количество списков данных страниц, ведь компания не платит заработную плату в соответствии с количество строк кода. Если данные вашего списка ограничены, вы можете использовать этот метод, чтобы увеличить количество отображаемых элементов списка. Если объем данных очень большой, а количество узлов превышает 20 000, как бы ни упрощали, этот способ неприменим.

2. Оптимизируйте использование setData

как图五показано, апплетsetDateпроизводительность будет зависеть отsetDataРазмер данных и ограничение частоты вызовов. Так вращаться вокруг сокращения каждый разsetDataразмер данных, уменьшитьsetDataЧастота звонков для оптимизации.

(1) Удалить лишние поля

Коллеги на бэкенде часто берут данные из базы данных и возвращают их напрямую во фронтенд без какой-либо обработки, поэтому это приводит к большой избыточности данных, а многие поля вообще не используются. нужно удалить эти поля, чтобы уменьшитьsetDateразмер данных.

(2)setDataрасширенное использование

Обычно, когда мы добавляем, удаляем или изменяем данные в данных, мы извлекаем исходные данные, обрабатываем их, а затем используем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Как быть с атрибутом isDisplay элементов с индексами от 0 до 9? Вы можете подумать об использовании цикла 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 до 9isDisplayсвойство изменено на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)

Что касается операции удаления, то лучшего способа я не нашел.

3. Используйте пользовательские компоненты

Вы можете инкапсулировать одну или несколько строк списка в пользовательский компонент и использовать компонент на странице списка, который учитывает только один узел, чтобы данные, которые может отображать ваш список, могли быть умножены. Количество узлов в компоненте тоже ограничено, но можно вкладывать компоненты послойно, чтобы добиться бесконечной загрузки списка, если не боитесь неприятностей

4. Используйте виртуальный список

После вышеуказанной серии операций производительность списка будет значительно улучшена, но если объем данных слишком велик,wxmlКоличество узлов также может превысить ограничение, что приведет к сбою страницы. Наш метод обработки заключается в использовании виртуального списка, страница отображает только текущую видимую область и узлы нескольких фрагментов данных выше и ниже видимой области и управляет отображением узлов через isDisplay.

  • Над видимой областью:above
  • Видимая область:screen
  • Ниже видимой области:below

图六  节点渲染示意图

1.listDataструктура массива

Используйте двумерный массив, потому что, если это одномерный массив, необходимо использовать прокрутку страницы.setDataУстановить много элементовisDispalyсвойства для управления отображением списка. в то время как 2D-массив можно вызвать один раз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дом структура

    <!-- 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. Получите высоту в пикселях первого слоя списка.

  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Количество узлов относительно стабильно, и данные рендеринга страницы могут немного колебаться из-за ошибки расчета индекса данных видимой области, но оно вообще не будет превышать лимит количества узлов на странице апплета. Теоретически список из 1 миллиона фрагментов данных не будет проблемой, если у вас хватит терпения и энергии загрузить в список столько данных.

7. Что нужно оптимизировать

  • Высота каждой строки списка должна быть фиксированной, иначе это вызовет ошибки в вычислении индекса данных видимой области.
  • После визуализации списка воспроизведения вернитесь к списку.Если скорость руки слишком высока, данные в верхней и нижней областях не будут отображаться, и появится короткий белый экран.Проблему белого экрана можно решить.prepareNum, throttleTimeДва параметра улучшены, но не могут быть полностью решены (после тестирования и сравнения обнаружено, что даже если список не обрабатывается, при слишком высокой скорости скольжения появляется короткий белый экран).
  • Если в списке есть изображения, верхняя и нижняя области перерендериваются, хотя изображения кэшируются локально и не требуют повторного запроса с сервера, повторный рендеринг все равно занимает время, особенно когда руки очень заняты. быстрый. Согласно вышеприведенной идее,isDisplayуничтожать только не-<image>Таким образом, количество узлов все равно будет увеличиваться, но оно должно удовлетворять потребности большинства проектов, в зависимости от того, что выберет ваш проект.

5. Сравнение использования пользовательских компонентов и виртуальных списков.

Хотя я не знаю почему, моя интуиция подсказывает мне, что производительность при использовании пользовательских компонентов будет относительно низкой. Чтобы сравнить преимущества и недостатки двух методов, мы использовалиTraceИнструмент выполняет тесты производительности на 5000 данных изображений полос.

Сравнение использования памяти:

Использование памяти пользовательскими компонентами:

图七   自定义组件内存占用情况

Использование памяти виртуального списка:

图八   虚拟列表内存占用情况

Из сравнения видно, что компонент не разрушается при подтягивании и загрузке компонента, что приводит к постепенному увеличению объема данных. Виртуальный список уничтожит такое же количество данных при добавлении данных, поэтому соотношение памяти будет стабильным на определенном уровне. Для этого тестового купола 5000 фрагментов данных используют пользовательские компоненты и, наконец, занимают 2000 МБ памяти, в то время как виртуальный список стабилен на уровне 700 МБ.

Сравнение времени повторного рендеринга после setData:

Повторный рендеринг пользовательского компонента занимает много времени:

图九   自定义组件重新渲染耗时

Время перерисовки виртуального списка:

图十   虚拟列表重新渲染耗时

Из результатов тестирования видно, что виртуальный список лучше, чем пользовательский компонент, независимо от того, является ли он трудоемким распределением, максимальным трудоемким и минимальным трудоемким.

Наконец, прикрепите адрес github виртуального списка.Если это будет вам полезно, не забудьте поставить маленькую звездочку

github