小程序动态高度长列表优化实战:解决滑动白屏与性能问题(一)

1,038 阅读4分钟

背景

在我之前的两个多端项目中,都涉及到了小程序的动态高度长列表优化问题。虽然在Web端,长列表优化已经有很多成熟的方案,例如react-virtualized,并且它也可以解决动态高度的问题。但在小程序端,却缺乏一些经验丰富的案例。因此,我决定总结这两个项目的长列表性能优化实战经验,希望能对大家有所帮助。

前言

关于长列表的解决方案,网上的资源非常丰富,你可能会问:现在还有必要单独写一篇关于长列表优化的文章吗?不就是虚拟列表方案吗?

首先,大部分网络资源都是针对 Web端 的长列表优化方案,而针对小程序的解决方案则相对较少。

如果你在小程序中遇到以下问题,不妨继续阅读本文,相信你会有意想不到的收获:

  • 长列表滑动时出现白屏现象
  • 即使使用了虚拟列表,仍然出现白屏现象(列表项复杂)
  • 动态高度问题

小程序卡顿原因分析

查看小程序的官方文档:

小程序性能指南.png

很容易得到结论 线程间的通信频次和数据量 以及 WXML 节点数 是制约小程序页面性能的关键因素。其中,WXML 节点数 又是制约页面滑动性能最主要因素

完全动态高度

案例一是一个重构项目,它的列表长这样,一个列表项会包含:富文本、九宫格图片、视频、音频、各类图文节点...总而言之,非常复杂,一个列表项,几乎包含了其他项目一整个页面的内容

显而易见的是,相比于重构前的项目有以下几个问题:

  • 1.x版本时期,由于列表项结构复杂,没有办法解决小程序长列表卡顿问题;
  • 2.x版本重构后,列表项更加复杂,不仅高度不一,还存在异步加载(列表项中的内容是通过接口返回的)带来的高度动态变化。Android机上,滑动10页不到,页面严重卡顿,渲染白屏;
  • 业内,并没有针对小程序动态高度长列表(列表项中的内容不是固定的),性能提升的成熟解决方案;

大家可以看下这未优化前的滑动性能

image.png

图片 5.png

华为Meta20上的表现:FPS≈46.5,滑动一分钟大概有10次严重的卡顿。FPS 曲线锯齿严重,内存上占用严重约 700+MB

方案

减少 WXML节点数

减少长列表节点数,最有效的方案就是虚拟列表,而虚拟列表就是要处理可视区和不可视区,我们可以模仿web端处理虚拟列表的方法来实现小程序中的虚拟列表

  • 通过列表滑动偏移量计算可视区需要渲染的列表项,分屏渲染列表数据,对于不可见区域节点,进行整屏删除
  • 数据渲染后,缓存列表项高度。列表项进入不可视区,会将整个列表项的节点删掉,同时创建一个空节点,并给空节点插入 style = { height: '缓存的高度' },这样避免列表因为列表项节点被清空导致抖动的问题

企业微信截图_20230108150942.png

关键代码

 // Taro.createIntersectionObserver 可以换成小程序的 api
 miniObserve = () => {
    const { wholePageIndex } = this.state
    const { scrollViewProps, listId, screenNum, isSkeleton } = this.props
    // 以传入的scrollView的高度为相交区域的参考边界,若没传,则默认使用屏幕高度
    const scrollHeight = scrollViewProps?.style?.height || this.windowHeight
    const observer = Taro.createIntersectionObserver(this.currentPage.page).relativeToViewport({
      top: screenNum * scrollHeight,
      bottom: screenNum * scrollHeight,
    })
    observer.observe(`#${listId} .wrap_${wholePageIndex}`, (res) => {
      if (isSkeleton) {
        this.handleObserveSkeletonResult(res?.intersectionRatio <= 0, wholePageIndex)
      } else {
        this.handleObserveNormalResult(res?.intersectionRatio <= 0, wholePageIndex)
      }
    })

    this.observers.push(observer)
  }

有几处细节的处理需要说明一下:

状态组件

必须用类组件来写,不能用函数式组件。因为只有类组件才可以保持状态,用来记录每一屏的 Observer,是展示还是隐藏

可视区显隐处理
  • 缓存高度,缓存高度的目的是当节点进入可视区后,获取到列表项的高度

再优化

经过可视区的显隐逻辑处理,性能上已经得到了很大的改善。我们继续对细节进行优化,使整个列表性能能够得到一个更好的表现

优化线程间的通信频次和数据量
  • 控制用户上拉加载更多的接口请求频率
  • 提前请求分页数据。比如用户滑动到一半的距离就可以请求下一页,不需要等用户滑动到了列表底部再请求数据
减少节点数

如果列表项中使用了 pupopdialog 这类的弹窗组件,可以进行懒加载处理。(也可以通过享元模式处理)

isShow && <Pupop visible={isShow} />
骨架图

当我们删除不可见区域的列表项节点后,会创建一个空节点,是一个纯色的 View,为了用户体验好一点,我们可以把纯色div改成骨架视图

// isVisible: 是否在可视区
<View>
    {
        isVisible ? <View>正常列表项内容</View> : <View>骨架图</View>
    }
</View>

成果

image.png

图片 6.png

华为Meta20上的表现:FPS≈53.6,滑动一分钟大概有1次严重的卡顿。FPS 曲线较平稳,内存占用稳定在 500MB 左右

最后

下一篇我们来说说不完全动态的小程序长列表优化,《小程序动态高度长列表优化实战:解决滑动白屏与性能问题(二)》

码字不易,各位老铁多支持: 点赞 + 收藏 + 评论

小程序性能检测工具:perfdog.qq.com

代码自取:github.com/lhanyun/lh-…