• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 减少首帧绘制时的冗余操作
2
3<!--Kit: Common-->
4<!--Subsystem: Demo&Sample-->
5<!--Owner: @mgy917-->
6<!--Designer: @jiangwensai-->
7<!--Tester: @Lyuxin-->
8<!--Adviser: @huipeizi-->
9
10## 应用冷启动与加载绘制首页
11
12应用冷启动即当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用。
13
14应用冷启动过程大致可分成以下四个阶段:应用进程创建&初始化、Application&Ability初始化、Ability生命周期、加载绘制首页。
15
16![](figures/ColdStart.png)
17
18**加载绘制首页**不仅是应用冷启动的四个阶段之一,还是首帧绘制最重要的阶段。而它可以分为三个阶段:加载页面、测量和布局、渲染。本文从这三个阶段入手,分成下面三个场景进行案例优化。
19
20![](figures/Render-FirstFrame.png)
21
22## 减少加载页面时间
23
24减少加载页面时间可以通过按需加载、减少自定义组件生命周期耗时两种方法来实现。
25
26### 按需加载
27
28按需加载可以避免一次性初始化和加载所有元素,从而使首帧绘制时加载页面阶段的创建列表元素时间大大减少,从而提升性能表现。具体可参考文档[列表场景性能提升实践](list-perf-improvment.md#懒加载)。
29
30**案例:每一个列表元素都被初始化和加载,为了突出效果,方便观察,设定数组中的元素有1000个,使其在加载页面阶段创建列表内元素耗时大大增加。**
31
32```ts
33@Entry
34@Component
35struct AllLoad {
36  @State arr: String[] = Array.from(Array<string>(1000), (val,i) =>i.toString());
37  build() {
38    List() {
39      ForEach(this.arr, (item: string) => {
40        ListItem() {
41          Text(`item value: ${item}`)
42            .fontSize(20)
43            .margin({ left: 10 })
44        }
45      }, (item: string) => item.toString())
46    }
47  }
48}
49```
50
51**优化:LazyForEach替换ForEach,避免一次性初始化和加载所有元素。**
52
53```ts
54class BasicDataSource implements IDataSource {
55  private listeners: DataChangeListener[] = [];
56  private originDataArray: string[] = [];
57
58  public totalCount(): number {
59    return 0;
60  }
61
62  public getData(index: number): string {
63    return this.originDataArray[index];
64  }
65
66  // 注册数据改变的监听器
67  registerDataChangeListener(listener: DataChangeListener): void {
68    if (this.listeners.indexOf(listener) < 0) {
69      console.info('add listener');
70      this.listeners.push(listener);
71    }
72  }
73
74  // 注销数据改变的监听器
75  unregisterDataChangeListener(listener: DataChangeListener): void {
76    const pos = this.listeners.indexOf(listener);
77    if (pos >= 0) {
78      console.info('remove listener');
79      this.listeners.splice(pos, 1);
80    }
81  }
82
83  // 通知组件重新加载所有数据
84  notifyDataReload(): void {
85    this.listeners.forEach(listener => {
86      listener.onDataReloaded();
87    })
88  }
89
90  // 通知组件index的位置有数据添加
91  notifyDataAdd(index: number): void {
92    this.listeners.forEach(listener => {
93      listener.onDataAdd(index);
94    })
95  }
96
97  // 通知组件index的位置有数据有变化
98  notifyDataChange(index: number): void {
99    this.listeners.forEach(listener => {
100      listener.onDataChange(index);
101    })
102  }
103
104  // 通知组件删除index位置的数据并刷新LazyForEach的展示内容
105  notifyDataDelete(index: number): void {
106    this.listeners.forEach(listener => {
107      listener.onDataDelete(index);
108    })
109  }
110
111  // 通知组件数据有移动
112  notifyDataMove(from: number, to: number): void {
113    this.listeners.forEach(listener => {
114      listener.onDataMove(from, to);
115    })
116  }
117}
118
119class MyDataSource extends BasicDataSource {
120  private dataArray: string[] = Array.from(Array<string>(1000), (val, i) => i.toString());
121
122  public totalCount(): number {
123    return this.dataArray.length;
124  }
125
126  public getData(index: number): string {
127    return this.dataArray[index];
128  }
129
130  public addData(index: number, data: string): void {
131    this.dataArray.splice(index, 0, data);
132    this.notifyDataAdd(index);
133  }
134
135  public pushData(data: string): void {
136    this.dataArray.push(data);
137    this.notifyDataAdd(this.dataArray.length - 1);
138  }
139}
140
141@Entry
142@Component
143struct SmartLoad {
144  private data: MyDataSource = new MyDataSource();
145
146  build() {
147    List() {
148      LazyForEach(this.data, (item: string) => {
149        ListItem() {
150          Text(`item value: ${item}`)
151            .fontSize(20)
152            .margin({ left: 10 })
153        }
154      }, (item:string) => item)
155    }
156  }
157}
158```
159
160使用SmartPerf Host工具抓取优化前后的性能数据进行对比。
161
162优化前页面Build耗时:
163
164![reduce-redundant-operations-when-render-first-frame-all-load](figures/reduce-redundant-operations-when-render-first-frame-all-load.png)
165
166优化后页面Build耗时:
167
168![reduce-redundant-operations-when-render-first-frame-smart-load](figures/reduce-redundant-operations-when-render-first-frame-smart-load.png)
169
170从trace图可以看出,使用ForEach时在Build阶段会创建所有元素,Build耗时65ms290μs,改为使用LazyForEach后Build耗时减少到745μs,性能收益明显。
171
172### 减少自定义组件生命周期时间
173
174LoadPage阶段需要等待自定义组件生命周期aboutToAppear的高耗时任务完成, 导致LoadPage时间大量增加,阻塞主线程后续的布局渲染,所以自定义组件生命周期的耗时任务应当转为Worker线程任务,优先绘制页面,避免启动时阻塞在startWindowIcon页面。
175
176**案例:自定义组件生命周期存在高耗时任务,阻塞主线程布局渲染。**
177
178```ts
179@Entry
180@Component
181struct TaskSync {
182  @State private text: string = '';
183  private count: number = 0;
184
185  aboutToAppear() {
186    this.text = 'hello world';
187    this.computeTask(); // 同步任务
188  }
189
190  build() {
191    Column({space: 10}) {
192      Text(this.text).fontSize(50)
193    }
194    .width('100%')
195    .height('100%')
196    .padding(10)
197  }
198
199  computeTask() {
200    this.count = 0;
201    while (this.count < 100000000) {
202      this.count++;
203    }
204    this.text = 'task complete';
205  }
206}
207```
208
209**优化:自定义组件生命周期的耗时任务转为Worker线程任务,优先绘制页面,再将Worker子线程结果发送到主线程并更新到页面。**
210
211```ts
212// TaskAsync.ets
213import { worker } from "@kit.ArkTS";
214
215@Entry
216@Component
217struct TaskAsync {
218  @State private text: string = '';
219  private workerInstance:worker.ThreadWorker = new worker.ThreadWorker('entry/ets/workers/worker.ets');
220
221  aboutToAppear() {
222    // 处理来自子线程的消息
223    this.workerInstance.onmessage = (message)=> {
224      console.info(`message from worker: ${JSON.stringify(message)}`);
225      this.text = JSON.parse(JSON.stringify(message)).data;
226      this.workerInstance.terminate();
227    }
228    this.text = 'hello world';
229    // 执行Worker线程任务
230    this.computeTaskAsync();
231  }
232
233  build() {
234    Column({space: 10}) {
235      Text(this.text).fontSize(50)
236    }
237    .width('100%')
238    .height('100%')
239    .padding(10)
240  }
241  private async computeTaskAsync(){
242    // 发送消息到子线程
243    this.workerInstance.postMessage('hello world')
244  }
245}
246```
247
248```ts
249// worker.ets
250import { worker } from "@kit.ArkTS";
251
252let parentPort = worker.workerPort;
253
254function computeTask(count: number) {
255  while (count < 100000000) {
256    count++;
257  }
258  return 'task complete';
259}
260// 处理来自主线程的消息
261parentPort.onmessage = (message) => {
262  console.info(`onmessage: ${JSON.stringify(message)}`);
263  // 发送消息到主线程
264  parentPort.postMessage(computeTask(0));
265}
266```
267
268使用SmartPerf Host工具抓取优化前后的性能数据进行对比。
269
270优化前loadpage耗时:
271
272![reduce-redundant-operations-when-render-first-frame-task-sync](figures/reduce-redundant-operations-when-render-first-frame-task-sync.png)
273
274优化后loadpage耗时:
275
276![reduce-redundant-operations-when-render-first-frame-task-async](figures/reduce-redundant-operations-when-render-first-frame-task-async.png)
277
278从trace图可以看出,优化前加载页面时loadpage耗时2s778ms807μs,其中主要耗时函数为自定义组件的生命周期函数aboutToAppear,将aboutToAppear中的耗时操作放到worker子线程中执行后,loadpage耗时减少到4ms745μs,页面加载时间大幅减少。
279
280## 减少布局时间
281
282减少布局时间可以通过异步加载和减少视图嵌套层次两种方法来实现。
283
284### 异步加载
285
286同步加载的操作,使创建图像任务需要在主线程完成,页面布局Layout需要等待创建图像makePixelMap任务的执行,导致布局时间延长。相反,异步加载的操作,在其他线程完成,和页面布局Layout同时开始,且没有阻碍页面布局,所以页面布局更快,性能更好。但是,并不是所有的加载都必须使用异步加载,建议加载尺寸较小的本地图片时将syncLoad设为true,因为耗时较短,在主线程上执行即可。
287
288**案例:使用Image组件同步加载高分辨率图片,阻塞UI线程,增加了页面布局总时间。**
289
290```ts
291@Entry
292@Component
293struct SyncLoadImage {
294  @State arr: String[] = Array.from(Array<string>(100), (val,i) =>i.toString());
295  build() {
296    Column() {
297      Row() {
298        List() {
299          ForEach(this.arr, (item: string) => {
300            ListItem() {
301              Image($r('app.media.4k'))
302                .border({ width: 1 })
303                .borderStyle(BorderStyle.Dashed)
304                .height(100)
305                .width(100)
306                .syncLoad(true)
307            }
308          }, (item: string) => item.toString())
309        }
310      }
311    }
312  }
313}
314```
315
316**优化:使用Image组件默认的异步加载方式加载图片,不阻塞UI线程,降低页面布局时间。**
317
318```ts
319@Entry
320@Component
321struct AsyncLoadImage {
322  @State arr: String[] = Array.from(Array<string>(100), (val,i) =>i.toString());
323    build() {
324      Column() {
325        Row() {
326          List() {
327            ForEach(this.arr, (item: string) => {
328              ListItem() {
329                Image($r('app.media.4k'))
330                  .border({ width: 1 })
331                  .borderStyle(BorderStyle.Dashed)
332                  .height(100)
333                  .width(100)
334              }
335            }, (item: string) => item.toString())
336          }
337        }
338      }
339  }
340}
341```
342
343使用SmartPerf Host工具抓取优化前后的性能数据进行对比。
344
345优化前布局耗时:
346
347![reduce-redundant-operations-when-render-first-frame-image-sync](figures/reduce-redundant-operations-when-render-first-frame-image-sync.png)
348
349优化后布局耗时:
350
351![reduce-redundant-operations-when-render-first-frame-image-async](figures/reduce-redundant-operations-when-render-first-frame-image-async.png)
352
353在优化前的trace图中可以看到,同步加载的每一张图片在参与布局时都会执行CreateImagePixelMap去创建图像,导致页面布局时间过长,FlushLayoutTask阶段耗时346ms458μs。图像使用异步加载进行优化后,页面布局时不再执行创建图像的任务,FlushLayoutTask阶段耗时减少到了2ms205μs,页面布局更快。
354
355### 减少视图嵌套层次
356
357视图的嵌套层次会影响应用的性能。通过减少不合理的容器组件,可以使布局深度降低,布局时间减少,优化布局性能,提升用户体验。
358
359**案例:通过Grid网格容器一次性加载1000个网格,并且额外使用3层Flex容器模拟不合理的深嵌套场景使布局时间增加。**
360
361```ts
362@Entry
363@Component
364struct Depth1 {
365  @State number: Number[] = Array.from(Array<number>(1000), (val, i) => i);
366  scroller: Scroller = new Scroller();
367
368  build() {
369    Column() {
370      Grid(this.scroller) {
371        ForEach(this.number, (item: number) => {
372          GridItem() {
373            Flex() {
374              Flex() {
375                Flex() {
376                  Text(item.toString())
377                    .fontSize(16)
378                    .backgroundColor(0xF9CF93)
379                    .width('100%')
380                    .height(80)
381                    .textAlign(TextAlign.Center)
382                    .border({width:1})
383                }
384              }
385            }
386          }
387        }, (item:string) => item)
388      }
389      .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
390      .columnsGap(0)
391      .rowsGap(0)
392      .size({ width: '100%', height: '100%' })
393    }
394  }
395}
396```
397
398**优化:通过Grid网格容器一次性加载1000个网格,去除额外的不合理的布局容器,降低布局时间。**
399
400```ts
401@Entry
402@Component
403struct Depth2 {
404  @State number: Number[] = Array.from(Array<number>(1000), (val, i) => i);
405  scroller: Scroller = new Scroller();
406
407  build() {
408    Column() {
409      Grid(this.scroller) {
410        ForEach(this.number, (item: number) => {
411          GridItem() {
412            Text(item.toString())
413              .fontSize(16)
414              .backgroundColor(0xF9CF93)
415              .width('100%')
416              .height(80)
417              .textAlign(TextAlign.Center)
418              .border({width:1})
419          }
420        }, (item:string) => item)
421      }
422      .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
423      .columnsGap(0)
424      .rowsGap(0)
425      .size({ width: '100%', height: '100%' })
426    }
427  }
428}
429```
430
431使用SmartPerf Host工具抓取优化前后的性能数据进行对比。
432
433优化前布局耗时:
434
435![reduce-redundant-operations-when-render-first-frame-view-nested-layout](figures/reduce-redundant-operations-when-render-first-frame-view-nested-layout.png)
436
437优化后布局耗时:
438
439![reduce-redundant-operations-when-render-first-frame-view-unnested-layout](figures/reduce-redundant-operations-when-render-first-frame-view-unnested-layout.png)
440
441根据trace图对比优化前后的布局时长,优化前FlushLayoutTask阶段耗时11ms48μs,优化后FlushLayoutTask耗时减少到5ms33μs,布局时间明显减少。
442
443## 减少渲染时间
444
445减少渲染时间可以通过条件渲染替代显隐控制的方法来实现。
446
447### 条件渲染
448
449使用visibility属性、if条件判断都可以控制元素显示与隐藏,但是初次加载时使用visibility属性隐藏元素也会创建对应组件内容,因此加载绘制首页时,如果组件初始不需要显示,建议使用条件渲染替代显隐控制,以减少渲染时间。关于条件渲染和显隐控制更多内容可以参考[合理选择条件渲染和显隐控制](./proper-choice-between-if-and-visibility.md)。
450
451**案例:初次渲染通过visibility属性隐藏Image组件,为了突出效果,方便观察,设置Image的数量为1000个。**
452
453```ts
454@Entry
455@Component
456struct VisibilityExample {
457  private data: number[] = Array.from(Array<number>(1000), (val, i) => i);
458
459  build() {
460    Column() {
461      // 隐藏不参与占位
462      Text('None').fontSize(9).width('90%').fontColor(0xCCCCCC)
463      Column() {
464        ForEach(this.data, () => {
465          Image($r('app.media.4k'))
466            .width(20)
467            .height(20)
468        })
469      }
470      .visibility(Visibility.None)
471    }.width('100%').margin({ top: 5 })
472  }
473}
474```
475
476**优化:通过条件渲染替代显隐控制。**
477
478```ts
479@Entry
480@Component
481struct IsVisibleExample {
482  @State isVisible: boolean = false;
483  private data: number[] = Array.from(Array<number>(1000), (val, i) => i);
484
485  build() {
486    Column() {
487      // 隐藏不参与占位
488      Text('None').fontSize(9).width('90%').fontColor(0xCCCCCC)
489      if (this.isVisible) {
490        Column() {
491          ForEach(this.data, () => {
492            Image($r('app.media.4k'))
493              .width(20)
494              .height(20)
495          })
496        }
497      }
498    }.width('100%').margin({ top: 5 })
499  }
500}
501```
502
503使用SmartPerf Host工具抓取优化前后的性能数据进行对比。
504
505优化前页面Build耗时:
506
507![reduce-redundant-operations-when-render-first-frame-visibility-build](figures/reduce-redundant-operations-when-render-first-frame-visibility-build.png)
508
509优化前render_service首帧耗时:
510
511![reduce-redundant-operations-when-render-first-frame-visibility-rs](figures/reduce-redundant-operations-when-render-first-frame-visibility-rs.png)
512
513优化后Build耗时:
514
515![reduce-redundant-operations-when-render-first-frame-ifelse-build](figures/reduce-redundant-operations-when-render-first-frame-ifelse-build.png)
516
517优化后render_service首帧耗时:
518
519![reduce-redundant-operations-when-render-first-frame-ifelse-rs](figures/reduce-redundant-operations-when-render-first-frame-ifelse-rs.png)
520
521**说明**:在App泳道找到页面加载后第一个ReceiveVsync,其中的Trace标签H:MarshRSTransactionData携带参数transactionFlag,在render_service泳道找到相同transactionFlag的标签H:RSMainThread::ProcessCommandUni,其所属的ReceiveVsync时长就是render_service首帧耗时。
522
523从trace图可以看出,优化前使用Visibility.None隐藏图片后在Build阶段仍然有Image元素创建,Build耗时82ms230μs,使用if else隐藏图片后Build阶段耗时减少到660μs,显著减少页面加载耗时。同时优化前应用的render_service首帧耗时为10ms55μs,而优化后减少到了1ms604μs,渲染时间明显减少。