背景
在我之前的两个多端项目中,都涉及到了小程序的动态高度长列表优化问题。虽然在Web端,长列表优化已经有很多成熟的方案,例如react-virtualized,并且它也可以解决动态高度的问题。但在小程序端,却缺乏一些经验丰富的案例。因此,我决定总结这两个项目的长列表性能优化实战经验,希望能对大家有所帮助。
前言
关于长列表的解决方案,网上的资源非常丰富,你可能会问:现在还有必要单独写一篇关于长列表优化的文章吗?不就是虚拟列表方案吗?
首先,大部分网络资源都是针对 Web端 的长列表优化方案,而针对小程序的解决方案则相对较少。
如果你在小程序中遇到以下问题,不妨继续阅读本文,相信你会有意想不到的收获:
- 长列表滑动时出现白屏现象
- 即使使用了虚拟列表,仍然出现白屏现象(列表项复杂)
- 动态高度问题
小程序卡顿原因分析
查看小程序的官方文档:
很容易得到结论 线程间的通信频次和数据量 以及 WXML 节点数 是制约小程序页面性能的关键因素。其中,WXML 节点数 又是制约页面滑动性能最主要因素
完全动态高度
案例一是一个重构项目,它的列表长这样,一个列表项会包含:富文本、九宫格图片、视频、音频、各类图文节点...总而言之,非常复杂,一个列表项,几乎包含了其他项目一整个页面的内容
显而易见的是,相比于重构前的项目有以下几个问题:
- 1.x版本时期,由于列表项
结构复杂,没有办法解决小程序长列表卡顿问题; - 2.x版本重构后,列表项
更加复杂,不仅高度不一,还存在异步加载(列表项中的内容是通过接口返回的)带来的高度动态变化。Android机上,滑动10页不到,页面严重卡顿,渲染白屏; - 业内,并没有针对小程序动态高度长列表(列表项中的内容不是固定的),性能提升的成熟解决方案;
大家可以看下这未优化前的滑动性能
在华为Meta20上的表现:FPS≈46.5,滑动一分钟大概有10次严重的卡顿。FPS 曲线锯齿严重,内存上占用严重约 700+MB
方案
减少 WXML节点数
减少长列表节点数,最有效的方案就是虚拟列表,而虚拟列表就是要处理可视区和不可视区,我们可以模仿web端处理虚拟列表的方法来实现小程序中的虚拟列表
- 通过
列表滑动偏移量计算可视区需要渲染的列表项,分屏渲染列表数据,对于不可见区域节点,进行整屏删除 - 数据渲染后,
缓存列表项高度。列表项进入不可视区,会将整个列表项的节点删掉,同时创建一个空节点,并给空节点插入style = { height: '缓存的高度' },这样避免列表因为列表项节点被清空导致抖动的问题
关键代码
// 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,是展示还是隐藏
可视区显隐处理
- 缓存高度,缓存高度的目的是当节点进入可视区后,获取到列表项的高度
再优化
经过可视区的显隐逻辑处理,性能上已经得到了很大的改善。我们继续对细节进行优化,使整个列表性能能够得到一个更好的表现
优化线程间的通信频次和数据量
- 控制用户上拉加载更多的接口请求频率
- 提前请求分页数据。比如用户滑动到一半的距离就可以请求下一页,不需要等用户滑动到了列表底部再请求数据
减少节点数
如果列表项中使用了 pupop、dialog 这类的弹窗组件,可以进行懒加载处理。(也可以通过享元模式处理)
isShow && <Pupop visible={isShow} />
骨架图
当我们删除不可见区域的列表项节点后,会创建一个空节点,是一个纯色的 View,为了用户体验好一点,我们可以把纯色div改成骨架视图
// isVisible: 是否在可视区
<View>
{
isVisible ? <View>正常列表项内容</View> : <View>骨架图</View>
}
</View>
成果
在华为Meta20上的表现:FPS≈53.6,滑动一分钟大概有1次严重的卡顿。FPS 曲线较平稳,内存占用稳定在 500MB 左右
最后
下一篇我们来说说不完全动态的小程序长列表优化,《小程序动态高度长列表优化实战:解决滑动白屏与性能问题(二)》
码字不易,各位老铁多支持: 点赞 + 收藏 + 评论
小程序性能检测工具:perfdog.qq.com