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 17 18**加载绘制首页**不仅是应用冷启动的四个阶段之一,还是首帧绘制最重要的阶段。而它可以分为三个阶段:加载页面、测量和布局、渲染。本文从这三个阶段入手,分成下面三个场景进行案例优化。 19 20 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 165 166优化后页面Build耗时: 167 168 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 273 274优化后loadpage耗时: 275 276 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 348 349优化后布局耗时: 350 351 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 436 437优化后布局耗时: 438 439 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 508 509优化前render_service首帧耗时: 510 511 512 513优化后Build耗时: 514 515 516 517优化后render_service首帧耗时: 518 519 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,渲染时间明显减少。