Taro高性能虚拟列表
这里记录一种我在Taro上探索出的一种高性能虚拟列表;因为业务需求和Taro框架本身的一些性能问题,在尝试了多种方案之后,找到了一种我认为还算比较高性能的长列表解决方案;
关于长列表渲染的一些方案
- 直接渲染:最基本的方式,一般适合不复杂的纯文本展示,能承载很大的数据量,但是也不建议使用;
- 分页渲染:采用上拉加载的方式分页加载数据,这种会随着上拉加载的数据量不断增加出现性能问题;能满足大部分不复杂的展示需求;
- 虚拟列表:只渲染屏幕"可视区域"的内容,通过占位隐藏不可见区域的内容;实现方式基本都是通过"计算"实现,只是实现计算的方式不一样而已;
先看页面
- 每个ListItem中的信息还是比较多的
- 每个卡片都可编辑
- 同事会展示上百个商品卡片
- 每个靠还存在大量的判断渲染、交互
- 卡片高度不固定
-
- 卡片高度不固定
遇到的问题
遇到的问题和测试结果都是在Taro 微信小程序中
经过测试发现,该页面同事渲染10个左右的卡片就能看收到明显的卡顿问题;
- 普通的分页加载上拉比较多的数据的时候就卡了;
- 二位数组方式,代码逻辑比较复杂,而且对与快速滑动和一键回到顶部不太友好;
- 官方提供的方案只适用于卡片高度固定的情况;
createIntersectionObserver
可以监听元素和另个元素是否相交,我们可以通过ListItem与屏幕容器是否相交来判断卡片是否需要显示;
上代码
jsx
import { ReactNode, useEffect, useRef, useState } from "react";
import { View } from "@tarojs/components";
import { createIntersectionObserver, pxTransform } from "@tarojs/taro";
import debounce from 'lodash/debounce';
interface IVirtualListProps {
/** 数据 */
dataSource: {
/** 唯一标识 使用该标识记录渲染到元素块 */
id: string
[key: string]: any
}[]
renderListitem: (data) => ReactNode
listItemHeight: (data: any) => number | number // 这里是为了实现卡片高度不固定的情况,同时保证获取卡片高度,用来占位使用,保证占位元素高度和数据渲染后一致,这样页面就不会抖动了
}
export default function VirtualList({ dataSource, renderListitem, listItemHeight }: IVirtualListProps) {
const lastItemRef = useRef<any>() // 最后一个item的ref,用来标记是否渲染完成,不然监听不到
const [showData, setShowData] = useState<{
[key: string]: {
show: boolean
height: number // 本来是想缓存高度的,这里没有,有优化空间
}
}>({})
const showDataRef = useRef<any>({});
const intersectionObserverRef = useRef<any>() // 只有一个对象,开始针对每个卡片设置一个对象,很占用cpu,时间久了手机会发烫,现在不会了
const updateShow = () => {
setShowData({ ...showDataRef.current })
}
const debounceUpdateShow = debounce(updateShow, 200); function observe() {
// console.log("ref.current", lastItemRef.current, typeof lastItemRef.current)
if (typeof lastItemRef.current === 'object') { // 最后一个渲染好了再监听
// console.log('intersectionObserver')
setTimeout(() => {
intersectionObserverRef.current.relativeToViewport().observe('.observe', (res) => {
// console.log('observe', res.boundingClientRect.height, res.id, res.intersectionRatio);
showDataRef.current[res.id] = { show: res.intersectionRatio > 0, height: res.boundingClientRect.height }; // 增加防抖批量一次更新,这里是为了在快速滑动的时候不停的改变状态的问题
debounceUpdateShow()
})
}, 100)
} else {
setTimeout(() => {
observe(); // 这里是一个循环,确保整个列表节点渲染完成,不然有些后面渲染的节点会监听不到
}, 100)
}
}
useEffect(() => {
if (dataSource.length) {
setShowData({})
showDataRef.current = {}; intersectionObserverRef.current = createIntersectionObserver(this, { observeAll: true }); observe()
} return () => {
if (intersectionObserverRef.current) {
intersectionObserverRef.current.disconnect() //销毁监听对象,不然会创建很多个,手机会发烫
}
} // 每次数据更新要从新监听 要重新建立对象
}, [dataSource])
const getHeight = (data) => {
// 获取卡片高度,用来占位使用,保证占位元素高度和数据渲染后一致,这样页面就不会抖动了
if (typeof listItemHeight === 'number') {
return listItemHeight
}
return listItemHeight(data)
}
const renderItem = (data) => {
if (showData[`observe_${data.id}`]?.show) {
return renderListitem(data)
}
// 占位图
return (
<View style={{
height: pxTransform(getHeight(data)),
width: "100%",
background: "url(https://static.tongliaowang.com/files/tlfiles/images/1707/12/20170712104436322556.jpg) top center no-repeat",
backgroundSize: "100% 100%"
}} />
)
}
return (
<View>
{
dataSource.map((item, i) => {
lastItemRef.current = dataSource && dataSource.length && (i === dataSource.length - 1);
return (
<View key={item.goodsCode}
ref={lastItemRef} style={{ width: '100%', height: pxTransform(getHeight(item)) }}
>
<View className='observe' id={`observe_${item.id}`}>
{renderItem(item)}
</View>
</View>
)
})
}
</View>
)
}
总结
这里还存在一些其他问题,比如快速滑动的时候会有白屏的情况(展示的全是占位图);高度可以通过缓存优化;能不能结合分解一起使用,一起使用的时候后来追加的元素监事件怎么解决;
无论列表渲染还是其他情况的渲染性能问题,其实都是因为同一时间渲染到更新的内容过多,我们要做的就是怎么渲染的节点树;
本来想放上视频效果呢,文档不支持视屏,gif效果还原度又不好;