我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Taro性能优化之复杂列表篇

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Taro性能优化之复杂列表篇

一、背景

随着项目的不断迭代,规模日益增大,而基于Taro3的运行时弊端也日渐凸显,尤其在复杂列表页面上表现欠佳,极度影响用户体验。本文将以复杂列表的性能优化为主旨,尝试建立检测指标,了解性能瓶颈,通过预加载、缓存、优化组件层级、优化数据结构等多种方式,实验后提供一些技术方案的建议,希望可以给大家带来一些思路。

二、问题现状及分析

我们以酒店某一多功能列表为例(下图),设定检测标准(setData次数及该setData的响应时效作为指标),检测情况如下:

指标

setData次数

渲染耗时(ms)

第一次进入列表页

7

2404

下拉长列表更新

3

1903

多屏列表下 筛选项更新

2

1758

多屏列表下 列表项更新

2

748

由于历史原因,该页面的代码,由微信的原生转成的taro1,后续迭代至taro3。项目中存在小程序原生写法可能忽略的问题。根据上面多次测出的指标值,以及视觉体验上来看,存在以下问题:

2.1  首次进入列表页的加载时间过长,白屏时间久

  • 列表页请求的接口时间过长;
  • 初始化列表也是setData数据量过大,且次数过多;
  • 页面节点数过多,导致渲染耗时较长;

2.2  页面筛选项的更新卡顿,下拉动画卡顿

  • 筛选项中节点过多,更新时setData数据量大;
  • 筛选项的组件更新会导致页面跟着一起更新;

2.3  无限列表的更新卡顿,滑动过快会白屏

  • 请求下一页的时机过晚;
  • setData时数据量大,响应慢;
  • 滑动过快时,没有从白屏到渲染完成的过渡机制,体验欠佳;

三、尝试优化的方案

3.1  跳转预加载API:

通过观察小程序的请求可以发现,列表页请求中,有两个请求耗时较为长。

在Taro3的升级中,官方有提到预加载Preload,在小程序中,从调用 Taro.navigateTo 等路由跳转 API 后,到小程序页面触发 onLoad 会有一定延时(约300ms,如果是分包新下载则跳转时间更长),因此一些网络请求可以提前到发起跳转时一起去请求。于是我们在在跳转前,使用Taro.preload预先加载复杂列表的请求:

// Page A
const query = new Query({
// ...
})


Taro.preload({
RequestPromise: requestPromiseA({data: query }),
})
// Page B
componentDidMount() {
// 在跳转的过程中,发出请求,因为返回的是一个promise,所以需要在B页面承接:
Taro.getCurrentInstance().preloadData?.RequestPromise?.then(res => {
this.setState(this.processResData(res.data))
})
}

用同样的检测方式反复测试后,使用preload的时,能提前300~400ms提前拿到酒店的列表数据。

左边是没使用preload的旧列表,右边是预加载的列表,能明显看出预加载后的列表会快一些。

然而在实际的使用中我们发现preload存在部分缺陷,对于承接页面,如果接口较为复杂,会对业务流程的代码有一定的入侵。究其本质,是前置了网络请求,所以我们可以对网络请求部分加入缓存策略,即可达到该效果,且接入成本会大大降低。

3.2  合理运用setData

setData 是小程序开发中使用最频繁、也是最容易引发性能问题的API。setData 的过程,大致可以分成几个阶段:

  • 逻辑层虚拟 DOM 树的遍历和更新,触发组件生命周期和 observer 等;
  • 将 data 从逻辑层传输到视图层;
  • 视图层虚拟 DOM 树的更新、真实 DOM 元素的更新并触发页面渲染更新。

数据传输的耗时与数据量的大小正相关,旧的列表页第一次加载的时候,一共请求了4个接口,setData短时间里有6次,数据量偏大的有两次,我们尝试的优化方式为,将数据量大的两次分开,另外五次发现都是一些零散的状态和数据,可以作为一次。

指标

setData次数

setData耗时(ms)

减少耗时百分比

第一次进入列表页

3

2182

9.23%

进行完这一步的操作,平均能减少200ms左右,效果较小,因为页面的节点数没变,setData主要的耗时还分布于渲染时间。

3.3  优化页面的节点数

根据微信官方文档的说明,一个太大的节点树会增加内存使用的同时,样式重排时间上也会更长。建议一个页面节点数量应少于 1000 个,节点树深度少于 30 层,子节点数不大于 60 个。 

在微信开发者工具中分析该页面两个模块存在大量的节点数。一个是筛选项模块,一个是长列表的模块。因为这部分功能较多,且结构复杂,我们采用了选择性渲染。如在用户浏览列表式,筛选项不生成具体节点。点击展开筛选的时候再渲染出节点,对于页面列表的体验有一定程度的缓解。另一方面,对于整体布局的书写上,有意识的避免嵌套过深的写法,如RichText使用,部分选择图片代替等。

3.4  优化筛选项相关

3.4.1  改变动画方式

在重构筛选项的过程中,发现在一些机型上,小程序的动画效果不太理想,比如当打开筛选项tab的时候,需要实现一个向下拉出的效果,早期在实现的时候,会出现两个问题:

  • 动画会闪一下 然后再出现
  • 筛选页面节点过多时,点击响应过慢,用户体验差

旧的筛选项的动画是通过keyframes方式实现了一个fadeIn的动画,加在最外层,但是无论如何在动画出现的那一帧,都会闪一下。分析下来,因为keyframes执行动画造成的卡顿:

.filter-wrap {
animation: .3s ease-in fadeIn;
}


@keyframes fadeIn {
0% {
transform: translateY(-100%)
}
100% {
transform: translateY(0)
}
}

于是,尝试换了一种实现方式,通过transition来实现transfrom:

.filter-wrap {
transform: translateY(-100%);
transition: none;
&.active {
transform: translateY(0);
transition: transform .3s ease-in;
}
}

3.4.2  维护简洁的state

操作筛选项的时候,每操作一次都需要根据唯一id从筛选项的数据结构中循环遍历,去找到对应的item,改掉item的状态,然后将整个结构重新setState。官方文档中提到关于setState,应该尽量避免处理过大的数据,会影响页面的更新性能。 

针对这一问题,采取的办法是:

  • 预先将复杂的对象扁平化,示例如下:

{
"a": {
"subs": [{
"a1": {
"subs": [{
"id": 1
}]
}
}]
},
"b": {
"subs": [{
"id": 2
}]
},


// ...
}

扁平化后的筛选项数据结构:

{
"1": {
"id": 1,
"name": "汉庭",
"includes": [],
"excludes": [],
// ...
},
"2": {
// ...
},


// ...
}

  • 不改变原有的数据,利用扁平化后的数据结构维护一个动态的选中列表:

const flattenFilters = data => {
// ...


return {
[id]: {
id: 2,
name: "全季",
includes: [],
excludes: []
// ...
},


// ...
}
}


const filters = [], filtersSelected = {}
const flatFilters = flattenFilters(filters)


const onClickFilterItem = item => {


// 所有的操作需要先拿到扁平化的item
const flatItem = flatFilters[item.id]


if (filtersSelected[flatItem.id]) {
// 已选中,需要取消选中
delete filtersSelected[flatItem.id]
}
else {
// 未选中,需要选中
filtersSelected[flatItem.id] = flatItem
// 取消选中排斥项
const idsSelected = Object.keys(filtersSelected)
const idsIntersection = intersection(idsSelected, flatItem.selfExcludes) // 交集
if (idsIntersection.length) {
idsIntersection.forEach(id => {
delete filtersSelected[id]
})
}


// 其他逻辑 (快筛,关键词等)
}


this.setState({filtersSelected})
}

上面是一个简单的实现,前后对比,我们只需要维护一个很简单的对象,对其属性进行添加或者删除,性能有细微的提高,且代码更为简单整洁。在业务代码中,类似这种通过数据结构转换提升效率的地方有很多。

关于筛选项,可以对比下检测的平均数据,减少200ms~300ms,也会得到一些提升:

指标

setData耗时旧

setData耗时新

减少耗时百分比

长列表下筛选项展开

1023

967

5.47%

长列表下点击筛选项

1758

1443

17.92%

3.5  长列表的优化

早期酒店列表页引入了虚拟列表,针对长列表渲染一定数目的酒店。核心的思路是只渲染显示在屏幕的数据,基本实现就是监听 scroll 事件,并且重新计算需要渲染的数据,不需要渲染的数据留一个空的 div 占位元素。

  • 加载下一页有轻微的卡顿:

通过数据发现,下拉更新列表平均耗时1900ms左右:

指标

setData次数

setData耗时

下拉列表更新

3

1903

针对这个问题,解决方案是,提前加载下一页的数据,将下一页存入内存变量中。滚动加载的时候直接从内存变量中去取,然后setData更新到数据中。

  • 滑动速度过快会出现白屏(速度越快白屏时间越久,下方左图):虚拟列表的原理就是利用空的View去占位,当快速回滚的时候,渲染的时候当节点过于复杂,特别是酒店带有图片,渲染就会变慢,导致白屏,我们进行了三种方案的尝试:1)  使用动态的骨架图代替原有的View占位 下方图右:

2)  CustomWrapper

为了提升性能,官方推荐了CusomWrapper,它可以将包裹的组件与页面隔离,组件渲染时不会更新整个页面,由page.setData变为component.setData。

自定义组件是基于Shadow DOM实现的,对组件中的DOM和CSS进行了封装,使得组件内部与主页面的DOM保持了分离。图片中的#shadow-root是根节点,成为影子根,和主文档分开渲染。#shadow-root可以嵌套形成节点树(Shadow Tree)


#shadow-root

包裹的组件被隔离,这样内部的数据的更新不会影响到整个页面,可以简单看下低性能客户端下的表现。效果还是明显的,同一时间点击,右侧弹窗出现的耗时平均会快200ms ~ 300ms (同一机型同一环境下测出),机型越低端越明显。

(右侧是CustomWrapper下的)

3)  使用小程序原生组件

用小程序的原生组件去实现这个列表Item。原生组件绕过Taro3的运行时,也就是说,在用户对页面操作的时候,如果是taro3的组件,需要进行前后数据的diff计算,然后生产新的虚拟dom所需要的节点数据,进而调用小程序的api去对节点进行操作。原生组件绕过了这一些列的操作,直接是是底层小程序对数据的更新。所以,缩短了一些时间。可以看一下实现后的效果:

指标

setData次数(旧)

setData次数(新)

下拉列表更新

3

1

setData耗时(旧)

  setData耗时(新)

  减少耗时百分比

1903

836

56.07%

可以看出原生性能提升很大,平均更新列表缩短1s左右,但是使用原生也有缺点,主要表现为以下两个方面:

  • 组件包含的所有样式 需要按照小程序的规范写一遍,且与taro的样式相互隔离;
  • 在原生组件中无法使用taro的API,比如createSelectorQuery这种;

对比三种方案,性能提升逐步加强。考虑到使用Taro原本的意义在于跨端,如果使用原生,就没办法达到这个目的,不过我们在尝试是否可以通过插件,在编译时生成对应原生小程序的组件代码,以此解决这一问题,最终达到最优效果。

3.6  React.memo

当复杂页面子组件过多时,父组件的渲染会导致子组件跟着渲染,React.memo可以做浅层的比较防止不必要的渲染:

const MyComponent = React.memo(function MyComponent(props) {

})

React.memo为高阶组件。它与React.PureComponent非常相似,但它适用于函数组件,但不适用于 class 组件。

如果你的函数组件在给定相同props的情况下渲染相同的结果,那么你可以通过将其包装在React.memo中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

function MyComponent(props) {

}


function areEqual(prevProps, nextProps) {

}


export default React.memo(MyComponent, areEqual);

四、总结

本次复杂列表的性能优化我们前后经历较久,尝试了各种可能的优化点。从列表页的预加载,筛选项数据结构和动画实现的改变,到长列表的体验优化和原生的结合,提升了页面的更新和渲染效率,目前仍密切关注,继续保持探索。

以下为最终效果对比(右侧为优化后):


免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Taro性能优化之复杂列表篇

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Taro性能优化之复杂列表篇

本文将以复杂列表的性能优化为主旨,尝试建立检测指标,了解性能瓶颈,通过预加载、缓存、优化组件层级、优化数据结构等多种方式,实验后提供一些技术方案的建议,希望可以给大家带来一些思路。

Android性能优化系列篇UI优化

这篇文章主要为大家介绍了Android性能优化系列篇UI优化示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2022-11-13

Android性能优化之运算篇

运算篇1) Intro to Compute and Memory ProblemsAndroid中的Java代码会需要经过编译优化再执行的过程。代码的不同写法会影响到Java编译器的优化效率。例如for循环的不同写法会对编译器优化这段代码
2022-06-06

golang函数性能优化与代码复杂性

go语言函数优化与代码复杂性密切相关,优化函数性能有助于编写高效易维护的代码。优化方法包括减少函数调用次数、使用内联函数、避免不必要的分配和并发。保持函数简短、命名清晰、避免使用goto,并在必要时使用异常处理。Go 语言函数性能优化与代码
golang函数性能优化与代码复杂性
2024-04-27

谈谈性能优化之缓存篇

用户数增长,架构演变,数据量增大,开始考虑怎么去做性能优化。而性能优化的第一定律就是:优先考虑使用缓存。

前端工程化之H5性能优化篇

导读:从粗糙到精致,从简单到复杂,全球互联网Web App(网页应用)平均体积已增压到1.6Mb,随着音视频等富媒体内容的流量池膨胀,终端设备上的用户对网页装载速度尤其敏感。页面不能做到秒开,就会有大量用户选择离开。重视并改善网站性能,优化即时网页装载时间,加
前端工程化之H5性能优化篇
2017-09-19

Android性能优化典范之多线程篇

多线程在Android性能优化中起到非常重要的作用。通过合理地使用多线程,可以提高应用程序的响应速度,加快数据处理和计算速度,提升用户体验。以下是Android性能优化中多线程的一些典范:1. 合理选择线程池大小:线程池是管理线程的重要工具
2023-09-20

扩展列表性能优化的方法有哪些?(扩展列表的性能优化有哪些方法)

在当今数字化的时代,扩展列表的性能优化变得越来越重要。无论是在网页开发、移动应用还是其他领域,高效的扩展列表可以提升用户体验、提高系统性能并节省资源。那么,扩展列表的性能优化有哪些方法呢?一、数据分页加载当处理大量数据时,一次性
扩展列表性能优化的方法有哪些?(扩展列表的性能优化有哪些方法)
Java2024-12-16

Vue组件的渲染列表性能优化

Vue组件渲染列表性能优化在Vue组件中渲染大量列表数据时,可能会导致性能下降。本指南提供了优化技巧,包括:使用虚拟列表延迟加载数据分页条件渲染缓存数据优化列表项使用key或track-by减少事件侦听器使用Fragment
Vue组件的渲染列表性能优化
2024-04-02

抖音 Android 性能优化系列:Java OOM 优化之 NativeBitmap 方案

对于 Java 内存泄漏治理,业界已经有比较成熟的方案,这里不做介绍,本文主要针对第二点尝试进行分析和优化。

性能篇:网络通信优化之通信协议

微服务架构作为一种现代化的软件设计理念,已经成为了许多企业构建复杂系统的首选。它的核心理念是将一个大型的单体应用拆分成多个小而自治的服务,每个服务都专注于完成特定的业务功能。微服务架构的核心不仅仅是技术上的拆分,更重要的是其背后所蕴含的一系

PHP接口性能优化之数据序列化与反序列化优化(如何优化PHP接口中的数据序列化与反序列化?)

PHP接口性能优化涉及数据序列化和反序列化。选择合适的技术至关重要。JSON序列化轻巧快速,但冗长。序列化函数处理复杂数据,但较慢。自定义序列化类提供更精确的控制。对象缓冲和预序列化可提高加载速度。选择适当的数据格式和优化反序列化(使用json_decode、禁用错误抑制、使用严格类型)可显著提升性能。
PHP接口性能优化之数据序列化与反序列化优化(如何优化PHP接口中的数据序列化与反序列化?)
2024-04-02

Win10内置的反馈应用中:开始菜单复杂,程序列表需优化

Win10开始菜单是这款新系统的“招牌”功能之一,它吸取了Win7开始菜单和Win8.1磁贴元素的优点,融合成了新版菜单。经过多个版本的进化,目前这款开始菜单在功能和外观上都有了长足进步。不过,很多用户仍然编程客栈认
2023-06-17

Golang函数性能优化之代码复用与重构

优化 go 函数性能的 方法包括:代码复用:通过提取函数、使用闭包和接口,减少重复代码。重构:修改代码结构,提高可读性、可维护性和性能。实战案例表明,代码复用和重构可以显著提高函数性能,优化后的函数速度提升了约 28%。Go 中的函数性能优
Golang函数性能优化之代码复用与重构
2024-04-17

热门标签

编程热搜

编程资源站

目录