写点什么

干货 | Taro 虚拟列表最佳实践

  • 2021-07-31
  • 本文字数:5222 字

    阅读完需:约 17 分钟

干货 | Taro虚拟列表最佳实践

一、背景

最近组内小程序项目从 Taro1 迁移到了 Taro3,紧跟凹凸实验室的步伐,开发体验确实比版本 1 好了很多,完全支持 React 语法,没有了那么多鸡肋的限制,项目的可配置程度也大大放开,充分给予了开发者自由发挥的空间。


但是由于 Taro3 是运行时架构,是以牺牲页面部分性能为代价的,这也间接导致了我们的列表页异常卡顿,由于我们的列表页是一次性请求所有数据,然后进行渲染,所以页面节点初始化渲染的时候会渲染很多节点,再加上一些筛选项,不用说用户,卡顿已经让我们自己都忍受不了。此为背景。


本文我们会先分析页面卡顿的原因,然后寻找对应的一些解决方案,分析其可行性,最后结合前期的问题解析,给出一套最优的解决方案。

二、原因分析

1)页面节点过多,渲染时间变长,阻碍了用户快速操作的需求;

2)列表 setState 数据量太大,造成逻辑层与渲染层的通讯时间变长;

3)修改 state,例如点击列表筛选项,列表数据需要重新大量渲染,造成页面卡顿;

三、解决方案

方案一:后端分页

我们第一时间想到让接口分页,这样初始化渲染的时候就不会渲染大量节点,然后监听下拉到底时机,再依次渲染数据;


但是该方案第一时间被毙掉,原因:

  • 列表页接口不只有小程序在用,app 客户端也在共用同一套接口,如果想让接口变更,那么 app 客户端也跟着去修改逻辑(列表页的逻辑也挺复杂的),因为我们去尝试给客户端增加需求量,不太厚道; 

  • 就算说服了客户端和服务端一起去修改,我们页面的初始化速度提升了,但是随着页面上拉,数据加载越来越多,当加载到一定数量之后,再操作页面的筛选项,依然会导致操作卡顿;


总结:想让页面初始化以及数据全部加载完成之后不卡顿,除非减少 setState 的数据量以及减少页面总的渲染节点数量,因此只能采用虚拟列表。

方案二:官方虚拟列表(3.2.1 版本)


官方文档:https://docs.taro.zone/docs/virtual-list


原理:只渲染当前可视区域内的数据节点,监听页面可视区域,不在可视区域的节点不再渲染,这样一来就大大减少了页面节点渲染数量。


使用效果:团队第一时间尝试了虚拟列表,但是效果并不是非常理想,主要问题有以下几点:


  • 由于我们的列表内容不是所有的 Item 都是等高的,所以虚拟列表每次渲染的时候都会去动态计算每个 Item 的高度,造成列表高度变换抖动;

  • 上拉加载过程中偶尔会出现无限上滑加载的问题,造成页面紊乱;

  • 滑动速度太快会导致页面很长一段时间的白屏,体验不佳;


总结:已知问题需要官方团队去解决,但是要等,而且 Item 不等高,需要频繁动态计算 Item 高度的问题并不好解决,目前市面上也没有什么特别好的方案,因此该方案也被搁浅了。

四、方案分析

1)减少页面节点数量:只能采用虚拟列表,只渲染当前可视区域内的节点;

2)减少 setState 的数据量:能不能不每次都去全量 setState;

3)动态计算 Item 高度:每次都重新计算每个 Item 高度,计算量太大,也会阻碍页面渲染;


基于以上问题,我们团队最终出品了更佳(没有最佳,只有更佳)虚拟列表方案。

五、终极(更佳)方案

5.1 效果概览

动图预览:



主要看一下虚拟列表节点组成:

5.2 前期思考

1)继续采用监听可视区域,只渲染可视区域内的节点。


2)由于 Item 不等高问题,需要动态计算每个 Item 的高度,效果不佳,我们放弃。因为只渲染当前可视区域内的数据,那么能不能以每一屏的数据为一个维度(界限),当一屏数据渲染完成之后,记录一下该屏幕节点所占的整体高度,当该屏幕的节点再次进入可视区域,我们将记录下的高度重新赋予这一屏幕,这样是不是就减少了大量计算的工作?


3)为了减少 setState 的数据量,不在可视区域内的那些屏幕的数据,可否用该屏幕的高度(一个简单的对象数据结构)去占位?好像思路都能说的过去,那到底可不可行呢,下面我们来一探究竟吧。

5.3 Coding

格式化数据

首先我们需要外部传入列表数据 list,然后在组件内部加工一下,按照一屏一屏渲染的思路,暂且把 list 改为二维数组,一个维度就是一屏的数据;



export default class VirtialList extends Component { constructor(props) { super(props) this.state = { twoList: [], // 二维数组 } } componentDidMount() { // 接收外部传入的列表数据 const { list } = this.props // 将list格式化为二维数组 this.formatList(list) } initList = [] // 承载初始化的二维数组,该数组初始化完成之后就不会再变了,除非外部list变化 /** * 将列表格式化为二维 * @param list 列表 */ formatList(list) { // 用户可自定义二维数组每一个维度的数据量 const { segmentNum } = this.props let arr = [] const _list = [] // 二维数组副本 list.forEach((item, index) => { arr.push(item) if ((index + 1) % segmentNum === 0) { // 够一个维度的量就装进_list _list.push(arr) arr = [] } }) // 将分段不足segmentNum的剩余数据装入_list const restList = list.slice(_list.length * segmentNum) if (restList?.length) { _list.push(restList) } this.initList = _list this.setState({ twoList: _list.slice(0, 1), // 第一次渲染,只取第一个维度的数据 }) } render() { const { twoList, } = this.state // 渲染回调 const { onRender } = this.props return ( <ScrollView> <View className="zt-main-list"> { twoList?.map((item, pageIndex) => { return ( // 每一个屏幕都用一个节点包裹着 <View key={pageIndex} className={`wrap_${pageIndex}`}> { item.map((el, index) => { return onRender?.(el, (pageIndex * segmentNum + index), pageIndex) }) } </View> ) }) } </View> </ScrollView> ) }}
复制代码


设置屏幕高度

我们已将数据格式化为二维数组了,初始化渲染的时候只会渲染数组的第一维度,那么在该维度节点渲染完成之后,需要记录下该维度节点所占屏幕的一个高度。



state = { wholePageIndex: 0, // 每一屏为一个单位,屏幕索引}formatList(list) { // ... this.setState({ twoList: _list.slice(0, 1), }, () => { // 注意:放在下一个事件循环去获取节点,更有保障 Taro.nextTick(() => { this.setHeight() }) })}pageHeightArr = [] // 用来装每一屏的高度setHeight():void { const { wholePageIndex } = this.state const query = Taro.createSelectorQuery() query.select(`.wrap_${wholePageIndex}`).boundingClientRect() query.exec((res) => { this.pageHeightArr.push(res?.[0]?.height) })}
复制代码


上拉加载


利用 ScrollView 的 onScrollToLower 属性,监听列表上拉至底部,加载下一个维度的数据,塞入二维数组列表。



<ScrollView scrollY onScrollToLower={this.renderNext} lowerThreshold={250}>//...</ScrollView>
renderNext = () => { // 每次加载下一屏幕的数据,修改屏幕索引 const page_index = this.state.wholePageIndex + 1
this.setState({ wholePageIndex: page_index, }, () => { const { wholePageIndex, twoList } = this.state // 找到当前屏幕的对应的数据,塞入二维数组 twoList[wholePageIndex] = this.initList[wholePageIndex] this.setState({ twoList: [...twoList], }, () => { Taro.nextTick(() => { this.setHeight() }) }) })}
复制代码


监听可视区域


利用 observer 对象的监听方法 observe,监听当前可视区域,渲染对应维度的数据,那么不在可视区域内的数据要怎么处理呢?


这也是该组件最重要的一环,当不在可视区域内的数据,因为我们之前已经记录了该维度节点渲染之后的一个高度,那么我们就利用一个节点赋予对应的高度,进行占位!



setHeight() { //... this.observe()}observe = () => { const { wholePageIndex } = this.state // 外界用户传入的组件高度 const { scrollViewProps } = this.props // 以传入的scrollView的高度为相交区域的参考边界,若没传,则默认使用屏幕高度 const scrollHeight = scrollViewProps?.style?.height || this.windowHeight // 设定监听的范围,我们这里默认监听上下两个屏幕的高度 const observer = Taro.createIntersectionObserver(this.currentPage.page).relativeToViewport({ top: 2 * scrollHeight, bottom: 2 * scrollHeight, }) observer.observe(`.wrap_${wholePageIndex}`, (res) => { const { twoList } = this.state if (res?.intersectionRatio <= 0) { // 当没有与当前视口有相交区域,则将该屏的数据置为该屏的高度占位 twoList[wholePageIndex] = { height: this.pageHeightArr[wholePageIndex] } this.setState({ twoList: [...twoList], }) } else if (!twoList[wholePageIndex]?.length) { // 如果有相交区域,则将对应的维度的数据塞入二维数组 twoList[wholePageIndex] = this.initList[wholePageIndex] this.setState({ twoList: [...twoList], }) } })}render() { return ( <ScrollView> <View className="zt-main-list"> { twoList?.map((item, pageIndex) => { return ( <View key={pageIndex} className={`wrap_${pageIndex}`}> { item?.length > 0 ? ( <Block> { item.map((el, index) => { return onRender?.(el, (pageIndex * segmentNum + index), pageIndex) }) } </Block> ) : ( <View style={{'height': `${item?.height}px`}}></View> ) } </View> ) }) } </View> </ScrollView> )}
复制代码

六、性能提升

接下来是智行小程序机票列表页优化前跟优化后的几组数据对比:

列表页渲染时长

主要指的是页面航线列表的渲染总时间。


筛选项响应时间

主要指的是从点击页面下方筛选按钮时间开始算起,到底部浮层弹出的时间间隔,单位毫秒。

性能提升总结

可以看出在使用虚拟列表对页面进行优化之后,页面总的渲染性能会有一个质的提升,页面列表渲染速度提升了将近 45%,按钮点击响应速度提升了将近 50%。


目前我们只是针对航班列表使用了虚拟列表进行优化,页面中还有一个比较损耗性能的点是上方的日历列表,后期我们将把日历列表也改成虚拟列表,相信性能会更进一步提升。

七、总结

组件的实现比较简单,关键点就在于:

1)将列表数据格式化为二维数组; 

2)不在可视区域内的数据用{height: xx px}填充,减少了列表数据 setState 的量;

3)动态计算每一个屏幕的高度并记录,减少计算量;

八、最后

该组件支持列表内部节点不等高的棘手问题,目前已经用于生产环境,运行稳定 。如果这篇文章对你现在的开发有一些帮助,或者说给你带来了一些更好的思考,欢迎一起来讨论。


githubhttps://github.com/tingyuxuan2302/taro3-virtual-list

npm 包https://www.npmjs.com/package/taro-virtual-list

Taro 物料市场https://taro-ext.jd.com/plugin/view/60bf31e23ac107d9df4685cb


作者简介

不浪,携程高级前端开发工程师,关注前端热门技术,目前从事小程序的相关开发与优化。


本文转载自:携程技术(ID:ctriptech)

原文链接:干货 | Taro虚拟列表最佳实践

2021-07-31 07:003253

评论

发布
暂无评论
发现更多内容

微服务之负载均衡使用场景

互联网工科生

负载均衡 微服务

证券开源领航者!国金证券通过中国信通院可信开源治理“先进级”评估

新消费日报

KaiwuDB 联合信通院数据库应用创新实验室召开数据库技术研讨沙龙

KaiwuDB

史上最短苹果发布会;三星、LG、高通联手进军 XR 市场丨 RTE 开发者日报 Vol.74

声网

“敏捷教练进阶课程”12月2-3日 · A-CSM认证在线周末班【分时段模块化教学】CST导师亲授

ShineScrum捷行

CAEE2023 | 造物云×华为云共建电子电路智慧云工厂,引领产业创新发展浪潮

Geek_2d6073

如何构建适合自己的DevOps软件测试改进方案

DevOps和数字孪生

DevOps 软件测试 仿真建模

TDengine 受邀参加 CNCC 2023,大会现场展位前“人山人海”!

TDengine

tdengine 时序数据库

Navicat Premium 15 for Mac(数据库开发工具)修复密码保存问题 v15.0.30中文激活版

mac

数据库开发工具 苹果mac Windows软件 Navicat Premium 15

用友BIP数智干部管理——数管干部 人才强企

用友BIP

数智干部管理

行云创新加入深圳市人工智能行业协会

行云创新

AI编程 人工智能协会 行云创新加入人工智能协会 云原生+AI 云端IDE+大模型

NFTScan 发展成为 PlatON 网络最大验证者节点之一

NFT Research

NFT NFT\ NFTScan

权重修改与预训练模型的应用

百度开发者中心

人工智能 大模型

《数据安全与流通:技术、架构与实践》新书发布

星环科技

短视频不仅带火了直播/PK,也带着RTC走进了军备竞赛

X2Rtc

音视频 短视频 RTC

golang结构体内存对齐

fm

EasyDL平台的强大开发力量

百度开发者中心

大模型 #人工智能

星环科技分布式向量数据库Transwarp Hippo正式发布,拓展大语言模型时间和空间维度

星环科技

API商品数据接口调用实战

Noah

最新体育赛事视频直播系统开发解决方案,提高用户黏性

软件开发-梦幻运营部

文心一言 VS 讯飞星火 VS chatgpt (125)-- 算法导论10.6 6题

福大大架构师每日一题

福大大架构师每日一题

全网最详细4W字Flink全面解析与实践(下)

Java随想录

Java 大数据 flink

聊聊 Rust 变量,你学会了吗?

树上有只程序猿

rust

KaiwuDB 获山东省工信厅“信息化应用创新优秀解决方案”奖

KaiwuDB

第19期 | GPTSecurity周报

云起无垠

1.从零开始学Windows批处理编程系列之基础介绍

全栈工程师修炼指南

编程 windows 批处理 运维学习之路

酷克数据出席永洪科技用户大会 携手驱动商业智能升级

酷克数据HashData

用 Milvus 和 NVIDIA Merlin 搭建高效推荐系统

Zilliz

nvidia Milvus Zilliz 向量数据库

Privatus for Mac(网页缓存清理) 6.6.2永久激活版

mac

苹果mac Windows软件 Privatus 网页缓存清理软件

大模型训练中的AI合成数据应用

百度开发者中心

大模型 人工智能「 LLM

华为云GaussDB城市沙龙活动走进安徽,助力金融行业数字化转型

华为云开发者联盟

数据库 后端 华为云 华为云GaussDB 华为云开发者联盟

干货 | Taro虚拟列表最佳实践_架构_携程技术_InfoQ精选文章