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