1# 减少首帧绘制时的冗余操作 2 3## 应用冷启动与加载绘制首页 4 5应用冷启动即当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用。 6 7应用冷启动过程大致可分成以下四个阶段:应用进程创建&初始化、Application&Ability初始化、Ability生命周期、加载绘制首页。 8 9 10 11**加载绘制首页**不仅是应用冷启动的四个阶段之一,还是首帧绘制最重要的阶段。而它可以分为三个阶段:加载页面、测量和布局、渲染。本文从这三个阶段入手,分成下面三个场景进行案例优化。 12 13 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 158 159优化后页面Build耗时: 160 161 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 266 267优化后loadpage耗时: 268 269 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 341 342优化后布局耗时: 343 344 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 429 430优化后布局耗时: 431 432 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 501 502优化前render_service首帧耗时: 503 504 505 506优化后Build耗时: 507 508 509 510优化后render_service首帧耗时: 511 512 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,渲染时间明显减少。