语言层面的优化
使用明确的数据类型,避免使用模糊的数据类型,例如ESObject。
使用AOT模式
AOT就是提前编译,将字节码提前编译成机器码,这样可以充分优化,从而加快执行速度。
未启用AOT时,一边运行一边进行机器码的生成。
开启AOT后,设备能直接运行已经提前优化过的机器码,大大提高执行速度。
应用启动优化
应用阶段划分:
- 第一阶段:应用进程的创建和初始化
- 第二阶段:App和Ability的初始化
- 第三阶段:Ability生命周期
- 第四阶段:加载绘制首页
每个启动阶段的优化策略:
- 第一阶段优化:设置合适分辨率的应用图标
- 第二阶段优化:减少首页Ability或者Page中import的模块数量,不是首页必须的模块可以使用动态异步加载,如await import(‘Page’)
- 第三阶段优化:Ability生命周期方法中,对于耗时操作进行异步处理
- 第四阶段优化:延迟加载,减少不必要的首页内容。例如使用LazyForEach替换ForEach
LazyForEach替换ForEach
LazyForEach是一种懒加载的模式,在循环绘制组件时能显著提升页面的加载速度。结合cacheCount方法能控制列表的缓存数量,实现更优的滑动体验。
减少丢帧卡顿
- 避免在主线程上执行耗时操作:将耗时操作放在TaskPool或者Worker等后台进程中执行,从而防止主线程负载过高。
- 减少渲染进程的冗余开销:使用资源图代替绘制、合理使用renderGroup、尺寸位置使用整数。
- 减少试图嵌套层级
- 组件复用
- 控制状态变量关联的组件数量
- 在对象上谨慎使用状态变量进行关联
优化案例
组件转场动画推荐使用transition,不推荐使用animateTo方法。
减少animateTo方法的使用数量,一起变更比分开变更更加高效。
多次animateTo时统一更新状态变量,减少多次更新状态变量导致的开销。
使用RenderGroup可以缓存组件及其子组件的绘制,从而降低绘制负载优化渲染性能。适合没状态绑定的不变组件,如果有状态绑定,当状态变化时缓存就是失效了。
使用@Reusable标记复用组件,适用频繁创建和销毁的组件,或反复切换条件渲染的控制分支且控制分支中的组件子树结构相同。
不推荐使用更新单个状态变量的形式控制多个组件的更新(命令式);
推荐使用状态变量和组件一对一绑定的方式,以数据变更驱动组件的刷新(声明式);
合理控制状态变量更新范围,避免关联刷新大量组件。关联变化较大的状态变量可以通过对象组合成一个状态变量,针对渲染成本较高的组件建议使用独立的状态变量进行关联。
理解@Prop和@ObjectLink的区别:@Prop是深拷贝关联,@ObjectLink是浅拷贝关联,因此优先推荐使用@ObjectLink的方式,从而减少系统内存开销。
性能调优工具
Profile调优工具
合理使用布局
组件布局属性(width、height、padding、margin等)大小发生变化会导致受影响的整个组件树重新更新,而非布局属性(Color、BackgroundColor、opacity等)的变化仅影响组件自身。这一点跟Web界面的渲染机制类似,因此我们在更新界面元素时,尽量减少布局属性的变化,防止影响整个组件的重新渲染。如果某些组件需要经常变化,可以将组件置于一个固定布局(这个布局内,布局属性固定)内,将影响固定在这个固定布局内部,从而减少对其他组件的影响。
精简节点数
- 移除冗余节点
- 使用扁平化布局减少节点数
扁平化方法:通过将嵌套结构摊开,减少中间节点,从而提升渲染速度。常用方法有:
- 通过RelativeContainer 相对布局实现扁平化
- 通过锚点定位实现扁平化
- 通过Grid布局实现扁平化
利用布局边界减少计算
对于能够在初期给定宽高的组件,在进行UI描述时尽量给定宽高数值,能够减少由于容器尺寸变化造成的重新测算过程的性能。
合理控制元素的显示与隐藏
首次绘制时,if会根据是否为true决定是否创建组件,而visibility组件无论是否显示都要创建组件。
再次显示时,if由于首次没有创建组件,再次显示时会创建组件,并经过Measure和Layout阶段。而visibility组件已经创建过,只需要经过Measure和Layout阶段。
- 只有初始的一次渲染或者交互次数很少的情况下,建议使用if条件判断来控制元素的显示与隐藏效果,对于内存有较大提升。
- 如果会频繁响应显示与隐藏的交互效果,建议使用切换Visibility.None和Visibility.Visible来控制元素显示与隐藏,提高性能。
长列表使用懒加载与组件服用
ForEach适合内容长度确定,长度在2屏内的列表。
LazyForEach适合长度超过2屏的情况,且内容布局相对固定的情况,配合组件复用的方式减少滑动过程中组件的创建。
选择合适的布局组件
常用布局:
- 使用Row、Column构建线性布局。
- 使用Stack构建层叠布局。
- 使用Flex构建弹性布局。
- List既具备线性布局的特点,同时支持懒加载和滑动的能力。
- Grid/GridItem提供了宫格布局的能力,同时也支持懒加载和滑动能力。
- RelativeContainer是一种相对布局,通过描述各个内容组件间相互关系来指导内容元素的布局过程,可从横纵两个方面进行布局描述,是一种二维布局算法。
原则:
- 在相同嵌套层级的情况下,如果多种布局方式可以实现相同布局效果,优选低耗时的布局,如使用Column、Row替代Flex实现相同的单行布局。
- 在能够通过其他布局大幅优化节点数的情况下,可以使用高级组件替代,如使用RelativeContainer替代Row、Column实现扁平化布局,此时其收益大于布局组件本身的性能差距。
- 仅在必要的场景下使用高耗时的布局组件,如使用Flex实现折行布局、使用Grid实现二维网格布局等。
Scroll嵌套List场景下,给定List组件宽高
在使用Scroll容器组件嵌套List组件加载长列表时,若不指定List的宽高尺寸,则默认全部加载。
- List没有设置宽高时,List的所有子组件ListItem都会参与布局。
- List设置宽高,只有布局区域内的ListItem子组件会参与布局。
- List使用ForEach加载子组件时,无论是否设置List的宽高,都会加载所有子组件。
- List使用LazyForEach加载子组件时,没有设置List的宽高,会加载所有子组件,设置了List的宽高,会加载List显示区域内的子组件。
长列表加载性能优化
当用户使用LazyForEach在线加载含有图片等数据量比较大的资源时,可以考虑使用动态预加载来预防弱网以及快速滑动场景中出现的白块问题。
组件复用
组件复用生效的条件是:
- 自定义组件被@Reusable装饰器修饰,即表示其具备组件复用的能力;
- 在一个自定义父组件下创建出来的具备组件复用能力的自定义子组件,在可复用自定义组件从组件树上移除之后,会被加入到其自定义父组件的可复用节点缓存中;
- 在一个自定义父组件下创建可复用的子组件时,若其父自定义组件的可复用节点缓存中有对应类型的可复用子组件,会通过更新可复用子组件的方式,快速创建可复用子组件;
ForEach循环渲染会一次性加载全量数据,因此不支持组件复用。
名词介绍:
- @Reusable表示组件可以被复用,结合LazyForEach懒加载一起使用,可以进一步解决列表滑动场景的瓶颈问题,提供滑动场景下高性能创建组件的方式来提升滑动帧率。
- CustomNode是一种自定义的虚拟节点,它可以用来缓存列表中的某些内容,以提高性能和减少不必要的渲染。通过使用CustomNode,可以实现只渲染当前可见区域内的数据项,将未显示的数据项缓存起来,从而减少渲染的数量,提高性能。
- RecycleManager是一种用于优化资源利用的回收管理器。当一个数据项滚出屏幕时,不会立即销毁对应的视图对象,而是将该视图对象放入复用池中。当新的数据项需要在屏幕上展示时,RecycleManager会从复用池中取出一个已经存在的视图对象,并将新的数据绑定到该视图上,从而避免频繁的创建和销毁过程。通过使用RecycleManager,可以大大减少创建和销毁视图的次数,提高列表的滚动流畅度和性能表现。
- CachedRecycleNodes是CustomNode的一个集合,常是用于存储被回收的CustomNode对象,以便在需要时进行复用。
使用场景
- 列表滚动(本例中的场景):当应用需要展示大量数据的列表,并且用户进行滚动操作时,频繁创建和销毁列表项的视图可能导致卡顿和性能问题。在这种情况下,使用列表组件的组件复用机制可以重用已经创建的列表项视图,提高滚动的流畅度。
- 动态布局更新:如果应用中的界面需要频繁地进行布局更新,例如根据用户的操作或数据变化动态改变视图结构和样式,重复创建和销毁视图可能导致频繁的布局计算,影响帧率。在这种情况下,使用组件复用可以避免不必要的视图创建和布局计算,提高性能。
- 地图渲染:在地图渲染这种场景下,频繁创建和销毁数据项的视图可能导致性能问题。使用组件复用可以重用已创建的视图,只更新数据的内容,减少视图的创建和销毁,能有效提高性能。
使用规则
- 使用 @Reusable标识:@Reusable标识自定义组件具备可复用的能力,它可以被添加到任意的自定义组件上,但是开发者需要小心处理自定义组件的创建流程和更新流程以确保自定义组件在复用之后能展示出正确的行为;
- 缓存和复用范围:可复用自定义组件的缓存和复用只能发生在同一父组件下,无法在不同的父组件下复用同一自定义节点的实例。e.g. A组件是可复用组件,其也是B组件的子组件,并进入了B组件的可复用节点缓存中,但是在C组件中创建A组件时,无法使用B组件缓存的A组件;
- 组件结构无显著变化:自定义组件的复用带来的性能提升主要体现在节省了自定义组件的JS对象的创建时间并复用了自定义组件的组件树结构,若应用开发者在自定义组件复用的前后使用渲染控制语法显著的改变了自定义组件的组件树结构,那么将无法享受到组件复用带来的性能提升;
- 仅在特定场景触发:组件复用仅发生在存在可复用组件从组件树上移除并再次加入到组件树的场景中,若不存在上述场景,将无法触发组件复用。e.g. 使用ForEach渲染控制语法创建可复用的自定义组件,由于ForEach渲染控制语法的全展开属性,不能触发组件复用。
注意:需要注意的是复用组件中有@Builder自定义构建函数时,状态变量推荐使用按引用传递。@Builder装饰的函数默认按值传递,当传递的参数为状态变量时,状态变量的改变不会引起@Builder方法内的UI刷新。
并发优化
异步任务并发处理
- 利用TaskPool执行简单并行任务:将一些耗时的操作放入异步任务中处理,避免阻塞主线程,提升应用的响应速度;
- 利用Worker完成周期类耗时操作:Worker可以空跑在后台等待事件触发,周期触发耗时操作使用Worker,这样可以避免TaskPool频繁拉起影响性能。
组件异步加载
对于网络图片可使用Image组件异步加载(syncLoad设置为false)
代码逻辑优化
-
选择合适的数据结构
索引存取考虑使用array数组,hash查找考虑使用map,去重逻辑考虑使用set等;
有时开发者使用object变量作为容器去处理map的逻辑,可以考虑使用ArkTS提供的高性能容器类,直接使用HashMap;
遇到纯数值计算的场合,推荐使用TypedArray的数据结构,比如Int8Array、Int32Array、Float32Array、BigInt64Array等。 -
合理使用缓存
当某些运算结果会反复使用时,以空间换时间,提前缓存以便于下次调用。 -
注意对象new和delete的频率
new和delete可能会触发内存管理回收,占用CPU资源从而影响界面渲染的能力,需要根据情况调整其频次。尤其在循环代码中,频繁的new、delete更会带来恶化的性能表现,应该尽量将new/delete优化到循环外去处理。 -
延迟执行资源释放操作
将资源关闭和释放操作放在setTimeout函数中执行,使其延迟到系统相对空闲的时刻进行,可以避免在程序忙碌时段占用关键资源,提升整体性能及响应能力。 -
减小拖动识别距离
应用识别拖动手势事件时需要设置合理的拖动距离,设置不合理的拖动距离会导致滑动不跟手、响应时延慢等问题。针对此类问题可以通过设置distance大小来解决。
视觉感知优化
在耗时操作过程中增加动画,使得超过更加顺滑。