1# 应用开发性能优化入门引导
2
3## 概述
4
5在开发应用时,优化应用性能是至关重要的。本文将介绍应用开发过程中常见的一些性能问题,并提供相应的解决方案,配合相关参考示例,帮助开发者解决大部分性能问题。
6
7应用性能分析的方法划分为了**性能分析四要素**,下面将介绍如何使用性能分析四要素,解决应用开发过程中的性能问题。
8
9* **第一要素:合理使用并行化、预加载和缓存**,需要合理地使用并行化、预加载和缓存等方法,例如使用多线程并发、异步并发、Web预加载等能力,提升系统资源利用率,减少主线程负载,加快应用的启动速度和响应速度。
10
11* **第二要素:尽量减少布局的嵌套层数**,在进行页面布局开发时,应该去除冗余的布局嵌套,使用相对布局、绝对定位、自定义布局、Grid、GridRow等扁平化布局,减少布局的嵌套层数,避免系统绘制更多的布局组件,达到优化性能、减少内存占用的目的。
12
13* **第三要素:合理管理状态变量**,应该合理地使用状态变量,精准控制组件的更新范围,控制状态变量关联组件数量,控制对象级状态变量的成员变量关联组件数,减少系统的组件渲染负载,提升应用流畅度。
14
15* **第四要素:合理使用系统接口,避免冗余操作**,应该合理使用系统的高频回调接口,删除不必要的Trace和日志打印,避免注册系统冗余回调,减少系统开销。
16
17## 第一要素:合理使用并行化、预加载和缓存
18
19需要合理地使用并行化、预加载和缓存等方法,提升系统资源利用率,减少主线程负载,加快应用的启动速度和响应速度。
20
21### 使用并行化提升启动速度
22
23自定义组件创建完成之后,在build函数执行之前,将先执行[aboutToAppear](../reference/apis-arkui/arkui-ts/ts-custom-component-lifecycle.md#abouttoappear)生命周期回调函数。此时若在该函数中执行耗时操作,将阻塞UI渲染,增加UI主线程负担。因此,应尽量避免在自定义组件的生命周期内执行高耗时操作。在aboutToAppear生命周期函数内建议只做当前组件的初始化逻辑,对于不需要等待结果的高耗时任务,可以使用多线程处理该任务,通过并发的方式避免主线程阻塞;也可以把耗时操作改为异步并发或延后处理,保证主线程优先处理组件绘制逻辑。
24
25#### 使用多线程执行耗时操作
26
27在日常开发过程中经常会碰到这样的问题:主页的开发场景中有多个Tab页展示不同内容,在首次加载完主页后,切换到第二个Tab页时需要加载和处理网络数据,导致第二个Tab页的页面显示较慢,有较大的完成时延。
28
29碰到此类问题,可以在生命周期aboutToAppear中,使用多线程并发、[高效并发编程](efficient-concurrent-programming.md)、[多线程能力场景化示例实践](multi_thread_capability.md)的方法执行第二个Tab页的网络数据访问解析、数据加载等耗时操作,既可以提前完成数据加载,也不会影响主线程UI绘制和渲染。
30
31使用TaskPool进行耗时操作的示例代码如下:
32
33```typescript
34import taskpool from '@ohos.taskpool';
35
36aboutToAppear() {
37  // 在生命周期中,使用TaskPool加载和解析网络数据
38  this.requestByTaskPool();
39}
40
41@Concurrent
42getInfoFromHttp(): string[] {
43  // 从网络加载数据
44  return http.request();
45}
46
47requestByTaskPool(): void {
48  // 创建任务项
49  let task: taskpool.Task = new taskpool.Task(this.getInfoFromHttp);
50  try {
51    // 执行网络加载函数
52    taskpool.execute(task, taskpool.Priority.HIGH).then((res: string[]) => {
53  });
54  } catch (err) {
55    logger.error(TAG, "failed, " + (err as BusinessError).toString());
56  }
57}
58```
59
60其他多线程并发相关文章:
61
62* [利用native的方式实现跨线程调用](native-threads-call-js.md)
63
64
65#### 使用异步执行耗时操作
66
67问题:在aboutToAppear生命周期函数中,运行了业务数据解析和处理等耗时操作,影响了上一页面点击跳转该页面的响应时延。
68
69可以把耗时操作的执行从同步执行改为异步或者延后执行,[提升应用冷启动速度](improve-application-cold-start-speed.md),比如使用setTimeOut执行耗时操作,示例如下:
70
71```typescript
72aboutToAppear() {
73  // 在生命周期中,使用异步处理数据,延时大小视情况确定
74  setTimeout(() => {
75    this.workoutResult();
76  }, 1000)
77}
78
79workoutResult(): string[] {
80  // 处理需要展示的业务数据
81  let data: Data[] = [];
82  for(let i = 1; i < 100; i++) {
83    result += data[i];
84  }
85  return result;
86}
87```
88
89### 使用预加载提升页面启动和响应速度
90
91应该合理使用系统的预加载能力,例如Web组件的预连接、预加载、预渲染,使用List、Swiper、Grid、WaterFlow等组件的cachedCount属性实现预加载,使用条件渲染实现预加载)等,提升页面的启动和响应速度。
92
93#### 使用Web组件的预连接、预加载、预渲染能力
94
95当遇到Web页面加载慢的场景,可以使用Web组件的预连接、预加载、预渲染能力,使用[Web组件开发性能提升指导](performance-web-import.md),在应用空闲时间提前进行Web引擎初始化和页面加载,提升下一页面的启动和响应速度。
96
97示例代码如下:
98
99```typescript
100import webview from '@ohos.web.webview';
101
102preload() {
103  // Web组件引擎初始化
104  webview.WebviewController.initializeWebEngine();
105  // 启动预连接,连接地址为即将打开的网址
106  webview.WebviewController.prepareForPageLoad('https://www.example.com', true, 2);
107}
108```
109
110#### 使用cachedCount属性实现预加载
111
112推荐在使用List、Swiper、Grid、WaterFlow等组件时,配合使用cachedCount属性实现预加载,详情指导在[WaterFlow高性能开发指导](waterflow_optimization.md)、[Swiper高性能开发指导](swiper_optimization.md)、[Grid高性能开发指导](grid_optimization.md)、[列表场景性能提升实践](list-perf-improvment.md),示例代码如下所示:
113
114```typescript
115  private source: MyDataSource = new MyDataSource();
116
117  build() {
118    List() {
119      LazyForEach(this.source, item => {
120        ListItem() {
121          Text("Hello" + item)
122            .fontSize(50)
123            .onAppear(() => {
124              console.info("appear:" + item)
125            })
126        }
127      })
128    }.cachedCount(3) // 扩大数值appear日志范围会变大
129  }
130```
131
132#### 使用条件渲染实现预加载
133
134问题:页面布局复杂度较高,导致跳转该页面的响应时延较高。
135
136可以使用条件渲染的方式进行[合理选择条件渲染和显隐控制](proper-choice-between-if-and-visibility.md),添加页面的简单骨架图作为默认展示页面,等数据加载完成后再显示最终的复杂布局,加快点击响应速度。
137
138示例代码如下:
139
140```typescript
141import skeletonComponent from "./skeletonComponent";
142import businessComponent from "./businessComponent";
143
144@State isInitialized: boolean = false;
145
146build() {
147  // 当数据未就位时展示骨架图,提升点击响应速度,减少页面渲染时间
148  if(!this.isInitialized) {
149    // 网络数据未获取前使用骨架图
150    skeletonComponent();
151  } else {
152    // 数据获取后再刷新显示内容
153    businessComponent();
154  }
155}
156```
157
158### 使用缓存提升启动速度和滑动帧率
159
160在列表场景中,推荐使用LazyForEach+组件复用+缓存列表项的能力,替代Scroll/ForEach实现滚动列表场景的实现,加快页面启动速度,提升滑动帧率;在一些属性动画的场景下,可以使用renderGroup缓存提升属性动画性能;也可以使用显隐控制对页面进行缓存,加快页面的显示响应速度。
161
162#### 组件复用
163
164应用框架提供了组件复用能力,可复用组件从组件树上移除时,会进入到一个回收缓存区。后续创建新组件节点时,会复用缓存区中的节点,节约组件重新创建的时间。
165
166若业务实现中存在以下场景,并成为UI线程的帧率瓶颈,推荐使用组件复用,具体指导在[组件复用实践](component-recycle.md)、[列表场景性能提升实践](list-perf-improvment.md)、[组件复用总览](component-reuse-overview.md):
167
168* 列表滚动(本例中的场景):当应用需要展示大量数据的列表,并且用户进行滚动操作时,频繁创建和销毁列表项的视图可能导致卡顿和性能问题。在这种情况下,使用列表组件的组件复用机制可以重用已经创建的列表项视图,提高滚动的流畅度。
169* 动态布局更新:如果应用中的界面需要频繁地进行布局更新,例如根据用户的操作或数据变化动态改变视图结构和样式,重复创建和销毁视图可能导致频繁的布局计算,影响帧率。在这种情况下,使用组件复用可以避免不必要的视图创建和布局计算,提高性能。
170* 地图渲染:在地图渲染这种场景下,频繁创建和销毁数据项的视图可能导致性能问题。使用组件复用可以重用已创建的视图,只更新数据的内容,减少视图的创建和销毁,能有效提高性能。
171
172示例代码如下:
173
174```typescript
175// xxx.ets
176class MyDataSource implements IDataSource {
177  private dataArray: string[] = [];
178  private listener: DataChangeListener | undefined;
179  // ...
180}
181
182@Entry
183@Component
184struct MyComponent {
185  private data: MyDataSource = new MyDataSource();
186
187  aboutToAppear() {
188    for (let i = 0; i < 1000; i++) {
189      this.data.pushData(i.toString());
190    }
191  }
192
193  build() {
194    List({ space: 3 }) {
195      LazyForEach(this.data, (item: string) => {
196        ListItem() {
197          ReusableChildComponent({ item: item })
198        }
199      }, (item: string) => item)
200    }
201    .width('100%')
202    .height('100%')
203  }
204}
205
206@Reusable
207@Component
208struct ReusableChildComponent {
209  @State item: string = ''
210  // 复用时触发的生命周期
211  aboutToReuse(params: ESObject) {
212    this.item = params.item;
213  }
214
215  build() {
216    Row() {
217      Text(this.item)
218        .fontSize(20)
219        .margin({ left: 10 })
220    }.margin({ left: 10, right: 10 })
221  }
222}
223```
224
225#### 使用renderGroup缓存提升属性动画性能
226
227页面响应时,可能大量使用属性动画和转场动画,当复杂度达到一定程度之后,就有可能出现卡顿的情况。[renderGroup](reasonable-using-renderGroup.md)是组件通用方法,它代表了渲染绘制的一个组合。
228
229具体原理是在首次绘制组件时,若组件被标记为启用renderGroup状态,将对组件及其子组件进行离屏绘制,将绘制结果合并保存到缓存中。此后当需要重新绘制相同组件时,就会优先使用缓存而不必重新绘制了,从而降低绘制负载,进而加快响应速度。
230
231示例代码如下:
232
233```typescript
234// Index.ets
235import { IconItem } from './IconItem';
236
237// IconItem相关数据
238class IconItemSource {
239  image: string | Resource = ''
240  text: string | Resource = ''
241  // ...
242}
243
244@Entry
245@Component
246struct Index {
247  private iconItemSourceList: IconItemSource[] = [];
248
249  aboutToAppear() {
250    // 遍历添加IconItem的数据
251    this.iconItemSourceList.push(
252      new IconItemSource($r('app.media.img1'), `label1`),
253      new IconItemSource($r('app.media.img2'), `label2`),
254      new IconItemSource($r('app.media.img3'), `label3`)
255    );
256  }
257
258  build() {
259    Column() {
260      // IconItem放置在grid内
261      GridRow() {
262        ForEach(this.iconItemSourceList, (item: IconItemSource) => {
263          GridCol() {
264            IconItem({ image: item.image, text: item.text })
265              .transition(TransitionEffect.scale({ x: 0, y: 0 })
266                  .animation({ delay: 1000, duration: 1000 })
267                  .combine(TransitionEffect.rotate({ z: 1, angle: 180 })
268                  .animation({ duration: 1000 }))
269              )
270          }
271        })
272      }
273    }
274  }
275}
276
277// IconItem.ets
278@Component
279export struct IconItem {
280  build()  {
281    Flex()  {
282      Image(this.image)
283      Text(this.text)
284    }
285    // 在IconItem内开启renderGroup
286    .renderGroup(true)
287  }
288}
289```
290
291#### 使用显隐控制进行页面缓存
292
293控制元素显示与隐藏是一种常见的场景,使用Visibility.None、if条件判断等都能够实现该效果。其中if条件判断控制的是组件的创建、布局阶段,Visibility属性控制的是元素在布局阶段是否参与布局渲染。使用时如果使用的方式不当,将引起性能上的问题。
294如果会频繁响应显示与隐藏的交互效果,建议使用切换Visibility.NoneVisibility.Visible来[合理控制元素显示与隐藏](proper-choice-between-if-and-visibility.md),在组件无需展示的时候进行缓存,提高性能。
295
296示例代码如下:
297
298```typescript
299@State isVisible: boolean = true;
300
301build() {
302  Column() {
303    Button("Switch visible and hidden").onClick(() => {
304        this.isVisible = !this.isVisible;
305    })
306    Stack() {
307      Scroll() {
308        Column() {
309          Image($r('app.media.icon'))
310        }
311      }.visibility(this.isVisible ? Visibility.Visible : Visibility.None)// 使用显隐控制切换,不会频繁创建与销毁组件
312    }
313  }
314}
315```
316
317## 第二要素:尽量减少布局的嵌套层数
318
319在进行页面布局开发时,应该去除冗余的布局嵌套,使用相对布局、绝对定位、自定义布局、Grid、GridRow等扁平化布局,减少布局的嵌套层数,避免系统绘制更多的布局组件,达到[优化布局性能](reduce-view-nesting-levels.md)、减少内存占用的目的。
320
321### 移除冗余节点
322
323应该删除冗余的布局嵌套,例如build最外层的无用容器嵌套、无用的Stack或Column嵌套等,减少布局层数。
324
325#### 删除无用的Stack/Column/Row嵌套
326
327例如可能会在Row容器包含一个同样也是Row容器的子级。这种嵌套实际是多余的,并且会给布局层次结构造成不必要的开销。示例代码如下:
328
329```typescript
330// 反例
331Row() {
332  Row() {
333    Text()
334    Text()
335  }
336  Text()
337}
338
339// 正例
340Row() {
341  Text()
342  Text()
343  Text()
344}
345```
346
347#### 删除build函数中最外层无用容器嵌套
348
349在开发过程中,布局的实现往往嵌套使用大量的自定义组件,build中冗余的最外层无用容器会大大增强嵌套层级,应该删除。
350
351反例代码如下:
352
353```typescript
354@Entry
355@Component
356struct ComponentA {
357  build() {
358    Column() {
359      ComponentB();
360    }
361  }
362}
363
364@Component
365struct ComponentB {
366  build() {
367    Column() {
368      Text('');
369    }
370  }
371}
372```
373
374正例代码如下:
375
376```typescript
377@Entry
378@Component
379struct ComponentA {
380  build() {
381    Column() {
382      ComponentB();
383    }
384  }
385}
386
387@Component
388struct ComponentB {
389  build() {
390    Text('');
391  }
392}
393```
394
395### 使用扁平化布局减少节点数
396
397#### 使用Column/Row替代Flex构建线性布局
398
399由于Flex本身带来的二次布局的影响,Flex的性能明显低于Column和Row容器,因此推荐使用Column/Row替代Flex构建线性布局,具体指导在[Flex布局性能提升使用指导](flex-development-performance-boost.md)。
400
401反例代码如下:
402
403```typescript
404@Entry
405@Component
406struct MyComponent {
407  build() {
408    Flex({ direction: FlexDirection.Column }) {
409      Flex().width(300).height(200).backgroundColor(Color.Pink)
410      Flex().width(300).height(200).backgroundColor(Color.Yellow)
411      Flex().width(300).height(200).backgroundColor(Color.Grey)
412    }
413  }
414}
415```
416
417正例代码如下:
418
419```typescript
420@Entry
421@Component
422struct MyComponent {
423  build() {
424    Column() {
425      Row().width(300).height(200).backgroundColor(Color.Pink)
426      Row().width(300).height(200).backgroundColor(Color.Yellow)
427      Row().width(300).height(200).backgroundColor(Color.Grey)
428    }
429  }
430}
431```
432
433#### 使用Flex、List、Grid、RelativeContainer、绝对布局和自定义布局等构建复杂布局
434
435复杂布局提供了场景化的能力,[优化布局性能](reduce-view-nesting-levels.md)可解决一种或者多种布局场景:
436
437* 使用Flex构建弹性布局;
438* List既具备线性布局的特点,同时支持懒加载和滑动的能力;
439* Grid/GridItem提供了宫格布局的能力,同时也支持懒加载和滑动能力;
440* RelativeContainer是一种相对布局,通过描述各个内容组件间相互关系来指导内容元素的布局过程,可从横纵两个方面进行布局描述,是一种二维布局算法。
441
442反例代码如下:
443
444```typescript
445@Entry
446@Component
447struct AspectRatioExample12 {
448    @State children: Number[] = Array.from(Array<number>(900), (v, k) => k);
449
450    build() {
451      Scroll() {
452        Grid() {
453          ForEach(this.children, (item: Number[]) => {
454            GridItem() {
455              Stack() {
456                Stack() {
457                  Stack() {
458                    Text(item.toString())
459                  }.size({ width: "100%"})
460                }.backgroundColor(Color.Yellow)
461              }.backgroundColor(Color.Pink)
462            }
463          }, (item: string) => item)
464        }
465        .columnsTemplate('1fr 1fr 1fr 1fr')
466        .columnsGap(0)
467        .rowsGap(0)
468        .size({ width: "100%", height: "100%" })
469    }
470  }
471}
472```
473
474正例代码如下:
475
476```typescript
477@Entry
478@Component
479struct AspectRatioExample11 {
480  @State children: Number[] = Array.from(Array<number>(900), (v, k) => k);
481
482  build() {
483    Scroll() {
484      Grid() {
485        ForEach(this.children, (item: Number[]) => {
486          GridItem() {
487            Text(item.toString())
488          }.backgroundColor(Color.Yellow)
489        }, (item: string) => item)
490      }
491      .columnsTemplate('1fr 1fr 1fr 1fr')
492      .columnsGap(0)
493      .rowsGap(0)
494      .size({ width: "100%", height: "100%" })
495    }
496  }
497}
498```
499
500## 第三要素:合理管理状态变量
501
502应该合理地使用状态变量,[精准控制组件的更新范围](precisely-control-render-scope.md),控制状态变量关联组件数量上限,控制对象级状态变量的成员变量关联组件数,减少系统的组件渲染负载,提升应用流畅度。
503
504### 精准控制组件的更新范围
505
506在复杂页面开发的场景下,精准控制组件更新的范围对提高应用运行性能尤为重要。应该避免状态变量的滥用引起的容器组件的刷新,进而影响帧率。
507
508#### 使用指定宽高的容器限制刷新范围
509
510当在一个同时指定宽高的容器里改变容器内部的布局,那么只会在该容器内部做布局和测量更新,不会扩散影响到容器外面的组件。
511
512反例代码如下:
513
514```typescript
515struct StackExample {
516  @State isVisible: boolean = true;
517  private data: number[] = [];
518
519  aboutToAppear() {
520    for (let i: number = 0; i < Constants.IMAGE_TOTAL_NUM; i++) {
521      this.data.push(i);
522    }
523  }
524
525  build() {
526    Column() {
527      Button('Switch Hidden and Show').onClick(() => {
528        this.isVisible = !this.isVisible;
529      })
530
531      Stack() {
532        if (this.isVisible) {
533          Text('New Page').width(100).height(30).backgroundColor(0xd2cab3)
534        }
535      }.width(100) // 本案例以Stack容器为例,只指定了宽,会触发父容器组件重新布局计算,引起ForEach中文本测量。
536
537      ForEach(this.data, (item: number) => { // 由于Stack容器没有同时指定宽高,会扩散影响到这一层,引起Text的测量更新。
538        Text(`Item value: ${item}`)
539          .fontSize($r('app.integer.font_size_20'))
540          .width($r('app.string.layout_100_percent'))
541          .textAlign(TextAlign.Center)
542      }, (item: number) => item.toString())
543    }
544  }
545}
546```
547
548正例代码如下:
549
550```typescript
551struct StackExample2 {
552  @State isVisible: boolean = true;
553  private data: number[] = [];
554
555  aboutToAppear() {
556    for (let i: number = 0; i < Constants.IMAGE_TOTAL_NUM; i++) {
557      this.data.push(i);
558    }
559  }
560
561  build() {
562    Column() { // 父容器
563      Button('Switch Hidden and Show').onClick(() => {
564        this.isVisible = !this.isVisible;
565      })
566
567      Stack() {
568        if (this.isVisible) {
569          Text('New Page').width(100).height(30).backgroundColor(0xd2cab3)
570        }
571      }.width(100).height(30) // 在指定宽高的Stack容器内,内部的Text组件变化只会在容器内部做布局和测量更新,不会影响到容器外ForEach中的Text组件。
572
573      ForEach(this.data, (item: number) => { // Stack容器指定了宽高,不会影响到这一层兄弟节点
574        Text(`Item value: ${item}`)
575          .fontSize($r('app.integer.font_size_20'))
576          .width($r('app.string.layout_100_percent'))
577          .textAlign(TextAlign.Center)
578      }, (item: number) => item.toString())
579    }
580  }
581```
582#### 减少不必要的参数层次传递
583
584@State+@Prop、@State+@Link、@State+@Observed+@ObjectLink三种方案的实现方式是逐级向下传递状态,当共享状态的组件间层级相差较大时,会出现状态层层传递的现象。对于没有使用该状态的中间组件而言,这是“额外的消耗”。因此,对于跨越多层的状态变量传递,使用@Provide+@Consume方案更为合理。
585
586反例代码如下:
587
588```typescript
589// 父组件
590@Component
591struct componentParent{
592  @State data: Data = {};
593
594  aboutToAppear() {
595    // 获取子组件数据
596    this.data = getData();
597  }
598
599  build() {
600    Column() {
601      componentSon({ data: this.data })
602    }
603  }
604}
605
606// 子组件
607@Component
608struct componentSon{
609  // 获取传递参数
610  @Prop data: Data;
611
612  build() {
613    Column() {
614      Text(data.text)
615      componentGrandSon({ data: this.data })
616    }
617  }
618}
619
620@Component
621struct componentGrandSon{
622  // 获取传递参数
623  @Prop data: Data;
624
625  build() {
626    Column() {
627      Text(data.text)
628    }
629  }
630}
631```
632
633正例代码如下:
634
635```typescript
636// 父组件
637@Component
638struct componentParent{
639  @Provide('data') data: Data = {};
640
641  aboutToAppear() {
642    // 获取子组件数据
643    this.data = getData()
644  }
645
646  build() {
647    Column() {
648      componentSon({ data: this.data })
649    }
650  }
651}
652
653// 子组件
654@Component
655struct componentSon{
656  // 获取传递参数
657  @Consume("data") data: Data;
658
659  build() {
660    Column() {
661      Text(data.text)
662      componentGrandSon({ data: this.data })
663    }
664  }
665}
666
667@Component
668struct componentGrandSon{
669  // 获取传递参数
670  @Consume("data") data: Data;
671
672  build() {
673    Column() {
674      Text(data.text)
675    }
676  }
677}
678```
679
680#### 避免滥用@Provide+@Consume
681
682在父子组件关联的场景下,@Provide+@Consume开销要大于@State+@Prop/@Link,因此在该场景下推荐使用@State+@Prop/@Link的组合。
683
684反例代码如下:
685
686```typescript
687// 父组件
688@Component
689struct componentParent{
690  @Provide("data") data: Data = {};
691
692  aboutToAppear() {
693    // 获取子组件数据
694    this.data = getData();
695  }
696
697  build() {
698    Column() {
699      componentSon()
700    }
701  }
702}
703
704// 子组件
705@Component
706struct componentSon{
707  // 获取传递参数
708  @Consume("data") data: Data;
709
710  build() {
711    Column() {
712      Text(data.text)
713    }
714  }
715}
716```
717
718正例代码如下:
719
720```typescript
721// 父组件
722@Component
723struct componentParent{
724  @State data:Data = {};
725
726  aboutToAppear() {
727    // 获取子组件数据
728    this.data = getData();
729  }
730
731  build() {
732    Column() {
733      componentSon({ data: this.data })
734    }
735  }
736}
737
738// 子组件
739@Component
740struct componentSon{
741  // 获取传递参数
742  @Prop data:Data;
743
744  build() {
745    Column() {
746      Text(data.text)
747    }
748  }
749}
750```
751
752### 精准控制状态变量关联组件数量
753
754应该控制状态变量关联的组件数量,如果一个状态关联过多的组件,当这个变量更新时会引起过多的组件重新绘制渲染,建议关联数量限制在20个以内,达到[精准控制组件的更新范围](precisely-control-render-scope.md)。
755
756#### 控制状态变量关联组件数量
757
758反例代码如下:
759
760```typescript
761@Observed
762class Translate {
763  translateX: number = 20;
764}
765@Component
766struct Title {
767  @ObjectLink translateObj: Translate;
768  build() {
769    Row() {
770      Image($r('app.media.icon'))
771        .translate({
772          x: this.translateObj.translateX // this.translateObj.translateX used in two component both in Row
773        })
774      Text("Title")
775        .translate({
776          x: this.translateObj.translateX
777        })
778    }
779  }
780}
781@Entry
782@Component
783struct Page {
784  @State translateObj: Translate = new Translate();
785  build() {
786    Column() {
787      Title({
788        translateObj: this.translateObj
789      })
790      Stack() {
791      }
792      .translate({
793        x:this.translateObj.translateX // this.translateObj.translateX used in two components both in Column
794      })
795      Button("move")
796        .translate({
797          x: this.translateObj.translateX
798        })
799        .onClick(() => {
800          animateTo({
801            duration: 50
802          }, () => {
803            this.translateObj.translateX = (this.translateObj.translateX + 50) % 150
804          })
805        })
806    }
807  }
808}
809```
810
811正例代码如下:
812
813```typescript
814@Observed
815class Translate {
816  translateX: number = 20;
817}
818@Component
819struct Title {
820  build() {
821    Row() {
822      Image($r('app.media.icon'))
823      Text("Title")
824    }
825  }
826}
827@Entry
828@Component
829struct Page1 {
830  @State translateObj: Translate = new Translate();
831  build() {
832    Column() {
833      Title()
834      Stack() {
835      }
836      Button("move")
837        .onClick(() => {
838          animateTo({
839            duration: 50
840          }, () => {
841            this.translateObj.translateX = (this.translateObj.translateX + 50) % 150
842          })
843        })
844    }
845    .translate({ // the component in Column shares the same property translate
846      x: this.translateObj.translateX
847    })
848  }
849}
850```
851
852#### 控制对象级状态变量成员数量
853
854应该控制对象级状态变量的成员变量关联的组件数量。开发者封装一个数据结构类用于进行状态变量关联时,应该避免过多的成员变量关联大量ArkUI组件,这种情况下,当这个大对象的一个成员变量更新时,会导致所有关联这个大对象的组件都同时进行刷新,造成不必要的性能损耗,从而影响帧率。
855
856反例代码如下:
857
858```typescript
859@Observed
860class AnimationParams {
861  translateX: number = 0;
862  translateY: number = 0;
863  alpha: number = 1;
864  rotationX: number = 0;
865  rotationY: number = 0;
866  centerX: number = 0;
867  centerY: number = 0;
868  angle: number = 0;
869  scaleX: number = 1;
870  scaleY: number = 1;
871}
872
873@Entry
874@Component
875struct Page {
876  @State animationParam: AnimationParams = new AnimationParams();
877
878  build() {
879    Column() {
880      Row() {
881        Image($r('app.media.startIcon'))
882          .translate({
883            x: this.animationParam.translateX,
884            y: this.animationParam.translateY
885          })
886          .rotate({
887            x: this.animationParam.rotationX,
888            y: this.animationParam.translateY,
889            centerX: this.animationParam.centerX,
890            centerY: this.animationParam.centerY,
891            angle: this.animationParam.angle
892          })
893          .opacity(this.animationParam.alpha)
894          .scale({
895            x: this.animationParam.scaleX,
896            y: this.animationParam.scaleY,
897            centerX: this.animationParam.centerX,
898            centerY: this.animationParam.centerY
899          })
900          .animation({
901            duration: 3000
902          })
903      }
904
905      Button('点击播放动画')
906        .onClick(() => {
907          this.animationParam.translateX = 300;
908          this.animationParam.translateY = 200;
909          this.animationParam.rotationX = 90;
910          this.animationParam.rotationY = 90;
911          this.animationParam.centerX = 20;
912          this.animationParam.centerY = 20;
913          this.animationParam.angle = 270;
914          this.animationParam.alpha = 0.5;
915          this.animationParam.scaleX = 3;
916          this.animationParam.scaleY = 3;
917        })
918    }
919  }
920}
921```
922
923正例代码如下:
924
925```typescript
926@Observed
927class RotationAnimationParams {
928  rotationX: number = 0;
929  rotationY: number = 0;
930  centerX: number = 0;
931  centerY: number = 0;
932  angle: number = 0;
933}
934
935@Observed
936class TranslateAnimationParams {
937  translateX: number = 0;
938  translateY: number = 0;
939}
940
941@Observed
942class AlphaAnimationParams {
943  alpha: number = 1;
944}
945
946@Observed
947class ScaleAnimationParams {
948  scaleX: number = 1;
949  scaleY: number = 1;
950  centerX: number = 0;
951  centerY: number = 0;
952}
953
954@Entry
955@Component
956struct Page {
957  @State rotationAnimation: RotationAnimationParams = new RotationAnimationParams();
958  @State translateAnimation: TranslateAnimationParams = new TranslateAnimationParams();
959  @State alphaAnimation: AlphaAnimationParams = new AlphaAnimationParams();
960  @State scaleAnimation: ScaleAnimationParams = new ScaleAnimationParams();
961
962  build() {
963    Column() {
964      Row() {
965        Image($r('app.media.startIcon'))
966          .translate({
967            x: this.translateAnimation.translateX,
968            y: this.translateAnimation.translateY
969          })
970          .rotate({
971            x: this.rotationAnimation.rotationX,
972            y: this.rotationAnimation.rotationY,
973            centerX: this.rotationAnimation.centerX,
974            centerY: this.rotationAnimation.centerY,
975            angle: this.rotationAnimation.angle
976          })
977          .opacity(this.alphaAnimation.alpha)
978          .scale({
979            x: this.scaleAnimation.scaleX,
980            y: this.scaleAnimation.scaleY,
981            centerX: this.scaleAnimation.centerX,
982            centerY: this.scaleAnimation.centerY
983          })
984          .animation({
985            duration: 3000
986          })
987      }
988
989      Button('点击播放动画')
990        .onClick(() => {
991          this.rotationAnimation.rotationX = 90;
992          this.rotationAnimation.rotationY = 90;
993          this.rotationAnimation.centerX = 20;
994          this.rotationAnimation.centerY = 20;
995          this.rotationAnimation.angle = 270;
996
997          this.translateAnimation.translateX = 300;
998          this.translateAnimation.translateY = 200;
999
1000          this.alphaAnimation.alpha = 0.5;
1001
1002          this.scaleAnimation.scaleX = 3;
1003          this.scaleAnimation.scaleY = 3;
1004          this.scaleAnimation.centerX = 20;
1005          this.scaleAnimation.centerY = 20;
1006        })
1007    }
1008  }
1009}
1010```
1011
1012### 避免不必要的创建和读取状态变量
1013
1014避免不必要的创建和读取状态变量,减少性能损耗。
1015
1016#### 删除冗余的状态变量标记
1017
1018状态变量的管理有一定的开销,应在合理场景使用,普通的变量用状态变量标记可能会导致性能劣化。
1019
1020反例代码如下:
1021
1022```typescript
1023@Observed
1024class Translate {
1025  translateX: number = 20;
1026}
1027
1028@Entry
1029@Component
1030struct UnnecessaryState1 {
1031  @State translateObj: Translate = new Translate(); // 变量translateObj没有关联任何UI组件,不应该定义为状态变量
1032  @State buttonMsg: string = 'I am button'; // 变量buttonMsg没有关联任何UI组件,不应该定义为状态变量
1033
1034  build() {
1035  }
1036}
1037```
1038以上示例中变量translateObj、buttonMsg没有关联任何UI组件,不应该定义为状态变量,否则读写状态变量都会影响性能。
1039
1040```typescript
1041@Observed
1042class Translate {
1043  translateX: number = 20;
1044}
1045
1046@Entry
1047@Component
1048struct UnnecessaryState2 {
1049  @State buttonMsg: string = 'I am button';
1050
1051  build() {
1052    Column() {
1053      Button(this.buttonMsg) // 这里只是读取变量buttonMsg的值,没有任何写的操作
1054    }
1055  }
1056}
1057```
1058以上示例中变量buttonMsg仅有读取操作,没有修改过,没有修改过的状态变量不应该定义为状态变量,否则读状态变量会影响性能。
1059
1060正例代码如下:
1061
1062```typescript
1063@Observed
1064class Translate {
1065  translateX: number = 20;
1066}
1067
1068@Entry
1069@Component
1070struct NecessaryState {
1071  @State translateObj: Translate = new Translate(); // 同时存在读写操作,并关联了Button组件,推荐使用状态变量
1072  buttonMsg: string = 'I am button'; // 仅读取变量buttonMsg的值,没有任何写的操作,直接使用一般变量即可
1073
1074  build() {
1075    Column() {
1076      Button(this.buttonMsg)
1077        .onClick(() => {
1078          animateTo(
1079            {
1080              duration: 50
1081            }, () => {
1082            this.translateObj.translateX = (this.translateObj.translateX + 50) % 150; // 点击时给变量translateObj重新赋值
1083          })
1084        })
1085    }.translate({
1086      x:this.translateObj.translateX // 读取translateObj中的值
1087    })
1088  }
1089}
1090```
1091没有关联任何UI组件的状态变量和没有修改过的状态变量不应该定义为状态变量,直接使用一般变量即可,否则会影响性能。
1092
1093#### 避免在For/while等循环函数中重复读取状态变量
1094
1095状态变量的读取耗时远大于普通变量的读取耗时,因此要避免重复读取状态变量,而是应该放在循环外面读取,例如在打印For/while循环中打印状态变量的日志信息。
1096
1097反例代码:
1098
1099```typescript
1100import hiTraceMeter from '@ohos.hiTraceMeter';
1101
1102@Entry
1103@Component
1104struct Page {
1105  @State message: string = '';
1106
1107  build() {
1108    Column() {
1109      Button('点击打印日志')
1110        .onClick(() => {
1111          hiTraceMeter.startTrace('print', 1);
1112          for (let i = 0; i < 10; i++) {
1113            console.info(this.message);
1114          }
1115          hiTraceMeter.finishTrace('print', 1);
1116        })
1117    }
1118  }
1119}
1120```
1121抓取Trace图如下:
1122![](./figures/unnecessarystate.png)
1123
1124正例代码:
1125
1126```typescript
1127import hiTraceMeter from '@ohos.hiTraceMeter';
1128
1129@Entry
1130@Component
1131struct Page {
1132  @State message: string = '';
1133
1134  build() {
1135    Column() {
1136      Button('点击打印日志')
1137        .onClick(() => {
1138          hiTraceMeter.startTrace('print', 1);
1139          let logMessage: string = this.message;
1140          for (let i = 0; i < 10; i++) {
1141            console.info(logMessage);
1142          }
1143          hiTraceMeter.finishTrace('print', 1);
1144        })
1145    }
1146  }
1147}
1148```
1149抓取Trace图如下:
1150![](./figures/necessarystate.png)
1151
1152由此可见,使用普通变量代替状态变量在For/while循环中读取,可以减少耗时,因此在For/while循环中频繁读取变量时,可使用普通变量代替状态变量。
1153## 第四要素:合理使用系统接口,避免冗余操作
1154
1155应该合理使用系统的高频回调接口,删除不必要的Trace和日志打印,避免冗余操作,减少系统开销,[避免开发过程中的冗余操作](avoiding-redundant-operations.md)。
1156
1157### 避免在系统高频回调用进行冗余和耗时操作
1158
1159应该避免在onDidScroll、onAreaChange等系统高频的回调接口中进行冗余和耗时操作,这些接口在系统的每一帧绘制中都会执行回调操作,因此在这些接口中进行冗余和耗时操作会大量消耗系统资源,影响应用运行性能。
1160
1161#### 避免在系统高频回调用打印Trace
1162
1163Trace的打印是会额外消耗系统性能的,因此应该避免在这些系统高频回调接口中打印Trace,示例代码如下:
1164
1165```typescript
1166// 反例
1167import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
1168
1169@Component
1170struct NegativeOfOnDidScroll {
1171  private arr: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
1172
1173  build() {
1174    Scroll() {
1175      ForEach(this.arr, (item: number) => {
1176        Text("ListItem" + item)
1177          .width("100%")
1178          .height("100%")
1179      }, (item: number) => item.toString())
1180    }
1181    .width('100%')
1182      .height('100%')
1183      .onDidScroll(() => {
1184        hiTraceMeter.startTrace("ScrollSlide", 1002);
1185        // 业务逻辑
1186        // ...
1187        hiTraceMeter.finishTrace("ScrollSlide", 1002);
1188      })
1189  }
1190}
1191
1192// 正例
1193@Component
1194struct PositiveOfOnDidScroll {
1195  private arr: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
1196
1197  build() {
1198    Scroll() {
1199      List() {
1200        ForEach(this.arr, (item: number) => {
1201          ListItem() {
1202            Text("TextItem" + item)
1203          }
1204          .width("100%")
1205          .height(100)
1206        }, (item: number) => item.toString())
1207      }
1208      .divider({ strokeWidth: 3, color: Color.Gray })
1209    }
1210    .width('100%')
1211    .height('100%')
1212    .onDidScroll(() => {
1213      // 业务逻辑
1214      // ...
1215    })
1216  }
1217}
1218```
1219
1220#### 避免在系统高频回调用打印日志
1221
1222日志的打印是会额外消耗系统性能的,特别是有些日志还读取了状态变量的信息,会加剧资源开销,因此应该避免在这些系统高频回调接口中打印日志,示例代码如下:
1223
1224```typescript
1225// 反例
1226import { hilog } from '@kit.PerformanceAnalysisKit';
1227
1228@Component
1229struct NegativeOfOnDidScroll {
1230  private arr: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
1231
1232  build() {
1233    Scroll() {
1234      List() {
1235        ForEach(this.arr, (item: number) => {
1236          ListItem() {
1237            Text("TextItem" + item)
1238          }
1239          .width("100%")
1240          .height(100)
1241        }, (item: number) => item.toString())
1242      }
1243      .divider({ strokeWidth: 3, color: Color.Gray })
1244    }
1245    .width('100%')
1246    .height('100%')
1247    .onDidScroll(() => {
1248      hilog.info(1002, 'Scroll', 'TextItem');
1249      // 业务逻辑
1250      // ...
1251    })
1252  }
1253}
1254
1255// 正例
1256@Component
1257struct PositiveOfOnDidScroll {
1258  private arr: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
1259
1260  build() {
1261    Scroll() {
1262      List() {
1263        ForEach(this.arr, (item: number) => {
1264          ListItem() {
1265            Text("TextItem" + item)
1266          }
1267          .width("100%")
1268          .height(100)
1269        }, (item: number) => item.toString())
1270      }
1271      .divider({ strokeWidth: 3, color: Color.Gray })
1272    }
1273    .width('100%')
1274    .height('100%')
1275    .onDidScroll(() => {
1276      // 业务逻辑
1277      // ...
1278    })
1279  }
1280}
1281```
1282
1283### 删除冗余Trace和日志打印
1284
1285Trace和日志打印会比较消耗系统性能,因此应该避免冗余的Trace和日志打印。推荐在Release版本中,尽量删除所有Trace信息,删除Debug日志,减少额外的系统开销。
1286
1287#### 在Release版本中删除Trace
1288
1289Trace会比较消耗系统性能,建议在Release版本删除Trace打印。
1290
1291反例代码如下:
1292
1293```typescript
1294@Component
1295struct NegativeOfTrace {
1296  aboutToAppear(): void {
1297    hitrace.startTrace("HITRACE_TAG_APP", 1003);
1298    // 业务代码
1299    // ...
1300    hitrace.finishTrace("HITRACE_TAG_APP", 1003);
1301  }
1302  build() {
1303    // 业务代码
1304    // ...
1305  }
1306}
1307```
1308
1309正例代码如下:
1310
1311```typescript
1312@Component
1313struct PositiveOfTrace {
1314  aboutToAppear(): void {
1315    // 业务代码
1316    // ...
1317  }
1318  build() {
1319    // 业务代码
1320    // ...
1321  }
1322}
1323```
1324
1325#### 在Release版本中删除Debug日志
1326
1327虽然在Release版本中不会打印debug级别日志,但是如果在日志的入参中进行了参数拼接,字符串拼接的逻辑还会执行,会有冗余开销,因此为了[避免开发过程中的冗余操作](avoiding-redundant-operations.md),建议在Release版本删除Debug日志打印。
1328
1329反例代码如下:
1330
1331```typescript
1332@Component
1333struct NegativeOfDebug {
1334  @State string1: string = 'a';
1335  @State string2: string = 'b';
1336
1337  aboutToAppear(): void {
1338    hilog.debug(1004, 'Debug', (this.string1 + this.string2));
1339    // 业务代码
1340    // ...
1341  }
1342
1343  build() {
1344    // 业务代码
1345    // ...
1346  }
1347}
1348```
1349
1350正例代码如下:
1351
1352```typescript
1353@Component
1354struct PositiveOfDebug {
1355  aboutToAppear(): void {
1356    // 业务代码
1357    // ...
1358  }
1359  build() {
1360    // 业务代码
1361    // ...
1362  }
1363}
1364```
1365
1366### 避免设置冗余系统回调监听
1367
1368冗余的系统回调监听,会额外消耗系统开销去做计算和函数回调消耗。比如设置了onAreaChange,就算回调中没有任何逻辑,系统也会在C++侧去计算该组件的大小和位置变化情况,并且把结果回调到TS侧,额外消耗了系统开销。
1369
1370反例代码如下:
1371
1372```typescript
1373@Component
1374struct NegativeOfOnClick {
1375  build() {
1376    Button('Click', { type: ButtonType.Normal, stateEffect: true })
1377      .onClick(() => {
1378        hitrace.startTrace("ButtonClick", 1004);
1379        hilog.info(1004, 'Click', 'ButtonType.Normal')
1380        hitrace.finishTrace("ButtonClick", 1004);
1381        // 业务代码
1382        // ...
1383      })
1384      .onAreaChange((oldValue: Area, newValue: Area) => {
1385        // 无任何代码
1386      })
1387  }
1388}
1389```
1390
1391正例代码如下:
1392
1393```typescript
1394@Component
1395struct PositiveOfOnClick {
1396  build() {
1397    Button('Click', { type: ButtonType.Normal, stateEffect: true })
1398      .onClick(() => {
1399        // 业务代码
1400        // ...
1401      })
1402  }
1403```
1404
1405## 使用性能工具分析和定位问题
1406
1407学会合理使用工具进行问题分析和定位,提升问题解决效率。
1408
1409### 学会使用IDE的Profier工具定位问题
1410
1411通过使用Profier工具,定位应用开发过程中的各种性能问题,详细的使用方法可以参考文章:[性能分析工具CPU Profiler](application-performance-analysis.md)。
1412
1413### 使用SmartPerf-Host分析应用性能
1414
1415[SmartPerf-Host](performance-optimization-using-smartperf-host.md)是一款深入挖掘数据、细粒度展示数据的性能功耗调优工具,可采集CPU调度、频点、进程线程时间片、堆内存、帧率等数据,采集的数据通过泳道图清晰地呈现给开发者,同时通过GUI以可视化的方式进行分析。工具当前为开发者提供了五个分析模板,分别是帧率分析、CPU/线程调度分析、应用启动分析、TaskPool分析、动效分析。
1416
1417### 使用状态变量组件定位工具分析状态变量关联信息
1418
1419开发者可以使用[状态变量组件定位工具](state_variable_dfx_pratice.md)获取状态管理相关信息,例如自定义组件拥有的状态变量、状态变量的同步对象和关联组件等,了解状态变量影响UI的范围,写出高性能应用代码。
1420
1421### 使用常用trace使用指导协助定位性能问题
1422
1423本文旨在介绍[常用trace使用指导](common-trace-using-instructions.md),解释它们的含义和用途,并阐述如何通过这些Trace来识别潜在的性能问题。同时,还将详细介绍Trace的工作原理,帮助开发者更好地理解这些Trace及如何实现性能数据的采集和分析。通过本文的阅读,开发者将对Trace有一个深入的了解,为应用程序性能优化提供有力支持。