性能优化

当 DOM 结构复杂时,为了得到流畅的列表滚动体验,list组件的性能优化必不可缺

list组件的性能优化分为精简DOM层级复用list-item细粒度划分list-item关闭scrollpage四个方面

其中,精简DOM层级复用list-item是使用list组件必须遵循的优化原则,细粒度划分list-item关闭scrollpage适用于部分场景,详见下文

精简 DOM 层级

精简 DOM 层级,即减少 DOM 树的级数和分支上的 DOM 节点数。层级越少、数量越少,布局和绘制就会越快

因此,开发者需要尽量剔除 list 中无意义的包裹类标签和层级

复用 list-item

复用list-item,即列表中相同的 DOM 结构设置为同一type属性list-item,这是优化列表滚动体验的关键

细粒度划分 list-item

细粒度划分list-item,即列表中相同的 DOM 结构划分为尽可能小的列表元素(即list-item

示例如下:

假设开发者要实现这样的效果:商品按类别分类,展示多种类别

从业务角度,可按类别划分为不同type属性list-item

然而,当list-item复杂时,会出现卡顿现象。推荐抛开业务逻辑,划分为尽可能小的列表元素

示例代码如下:

  1. <template>
  2. <list class="tutorial-page" onscrollbottom="loadMoreData">
  3. <!-- 细粒度划分list-item -->
  4. <block for="productList">
  5. <!-- title -->
  6. <list-item type="title" if="$item.title" class="title {{$idx>0?'margin-top':''}}">
  7. <text>{{$item.title}}</text>
  8. </list-item>
  9. <!-- banner -->
  10. <list-item type="banner" if="$item.bannerImg" class="banner">
  11. <image src="{{$item.bannerImg}}"></image>
  12. </list-item>
  13. <!-- productMini -->
  14. <list-item type="{{'productMini'+$item.productMini.length}}" if="$item.productMini" class="product-mini-wrap">
  15. <!-- 在当前list-item中使用了for指令,因此需要动态设置list-item的type属性。确保相同type属性的list-item的DOM结构完全一致 -->
  16. <div for="value in $item.productMini" class="product-mini">
  17. <image src="{{value.img}}" class="product-mini-img"></image>
  18. <text>{{value.name}}</text>
  19. <text class="product-mini-brief">{{value.brief}}</text>
  20. <text class="product-mini-price">{{value.price}}</text>
  21. </div>
  22. </list-item>
  23. <!-- textHint -->
  24. <list-item type="textHint" if="$item.textHint" class="text-hint">
  25. <text>{{$item.textHint}} ></text>
  26. </list-item>
  27. </block>
  28. <!-- list底部的加载更多 -->
  29. <list-item type="loadMore" class="load-more">
  30. <progress type="circular"></progress>
  31. <text>加载更多</text>
  32. </list-item>
  33. </list>
  34. </template>

关闭 scrollpage

list组件支持属性scrollpage,默认关闭,标志是否将顶部页面中非list的元素随list一起滚动。开启scrollpage会降低list渲染性能

因此,在开发者开启scrollpage前,推荐先尝试将顶部页面中非list的元素,作为一种或多种type属性list-item,移入list中,从而达到关闭scrollpage提高渲染性能的目的

示例如下:

假设开发者要实现这样的效果:顶部 banner,banner 下方为常见列表,需要整屏滚动

开发者一般会将页面划分为 banner 和 list 两部分,然后开启listscrollpage属性,实现整屏滚动

然而,开启scrollpage会降低list渲染性能,推荐将顶部 banner 作为一种特殊type属性list-item,移入list中,关闭scrollpage

示例代码如下:

<template>
  <!-- 列表实现,监听列表的scrollbottom事件,列表滚动到底部时加载更多数据 -->
  <list class="tutorial-page" onscrollbottom="loadMoreData">
    <list-item type="banner" class="banner">
      <image src="../../Common/img/demo_large.png"></image>
    </list-item>

    <!-- 商品列表 -->
    <block for="productList">
      <list-item type="product" class="content-item" onclick="route($item.url)">
        <image class="img" src="{{$item.img}}"></image>
        <div class="text-wrap">
          <div class="top-line">
            <text class="text-name">{{$item.name}}</text>
            <text class="text-price">{{$item.price}}</text>
          </div>
          <text class="bottom-line">{{$item.brief}}</text>
        </div>
      </list-item>
    </block>

    <!-- list-item实现的加载更多,type属性自定义命名为loadMore -->
    <list-item type="loadMore" class="load-more">
      <progress type="circular"></progress>
      <text>加载更多</text>
    </list-item>
  </list>
</template>

list-item 懒加载

懒加载,简称lazyload,本质上是按需加载

在传统的页面中,常用lazyload优化网页的性能:

  • 实现:不加载全部页面资源,当资源即将呈现在浏览器可视区域时,再加载资源
  • 优点:加快渲染的同时避免流量浪费在框架中,开发者也可使用lazyload概念优化列表的渲染:

  • 实现:提前 fetch 请求足够的列表数据保存在内存变量memList中,当list滚动到底部时,从memList中提取部分数据来渲染list-item。当memList中数据不足时,提前 fetch 请求数据,填充memList

  • 优点:每次网络请求与页面渲染的数据量不一致,减少首屏渲染占用的 JS 执行时间,减少渲染后续list-item的等待时间示例如下:

假设开发者要实现这样的效果:一个商品列表,每次渲染 10 个商品

  • 渲染首屏时,请求数据保存在内存变量memList中,从memList中提取部分数据渲染列表
  • 加载更多时,首先检查memList中是否有足够数据,有则直接从memList中提取部分数据渲染,而不是直接进行网络请求,减少时间消耗。当memList中数据不足时,提前请求数据示例代码如下:
<template>
  <!-- 列表实现,监听列表的scrollbottom事件,列表滚动到底部时加载更多数据 -->
  <list class="tutorial-page" onscrollbottom="renderMoreListItem">
    <!-- 商品列表 -->
    <block for="productList">
      <list-item type="product" class="content-item">
        <image class="img" src="{{$item.img}}"></image>
        <div class="text-wrap">
          <div class="top-line">
            <text class="text-name">{{$item.name}}</text>
            <text class="text-price">{{$item.price}}</text>
          </div>
          <text class="bottom-line">{{$item.brief}}</text>
        </div>
      </list-item>
    </block>

    <list-item type="loadStatus" class="load-status">
      <progress type="circular" show="{{hasMoreData}}"></progress>
      <text show="{{hasMoreData}}">加载更多</text>
      <text show="{{!hasMoreData}}">没有更多了~</text>
    </list-item>
  </list>
</template>

<script>
  import {dataComponentListLazyload} from '../../Common/js/data'

  // 模拟fetch请求数据
  function callFetch (callback) {
    setTimeout(function () {
      callback(dataComponentListLazyload)
    }, 500)
  }

  // 内存中存储的列表数据
  let memList = []

  export default {
    private: {
      productList: [],
      hasMoreData: true,
      // 每次渲染的商品数
      size: 10,
      // 是否正在fetch请求数据
      isLoadingData: false
    },
    onInit () {
      this.$page.setTitleBar({ text: 'list-item懒加载' })
      // 获取数据并渲染列表
      this.loadAndRender()
    },
    /**
     * 请求并渲染
     */
    loadAndRender (doRender = true) {
      this.isLoadingData = true
      // 重新请求数据并根据模式判断是否需要渲染列表
      callFetch(function (resList) {
        this.isLoadingData = false
        if (!resList) {
          console.error(`数据请求错误`)
        }
        else if (!resList.length) {
          this.hasMoreData = false
        }
        else {
          memList = memList.concat(resList)
          if (doRender) {
            this._renderList()
          }
        }
      }.bind(this))
    },
    _renderList () {
      // 渲染列表
      if (memList.length > 0) {
        const list = memList.splice(0, this.size)
        this.productList = this.productList.concat(list)
      }
      if (memList.length <= this.size) {
        // 提前请求新的数据
        this.loadAndRender(false)
      }
    },
    /**
     * 滑动到底部时加载更多
     */
    renderMoreListItem () {
      if (!this.isLoadingData) {
        this._renderList()
      }
    }
  }
</script>

注意:避免在ViewModel的数据属性中定义memList。因为在ViewModel的数据属性中定义变量会触发set/get数据驱动定义,而memList作为暂时保存数据的变量,不需监听数据变化