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组件复用性能优化四建议: 15 16* **减少组件复用的嵌套层级**,如果在复用的自定义组件中再嵌套自定义组件,会存在节点构造的开销,且需要在每个嵌套的子组件中的aboutToReuse方法中实现数据的刷新,造成耗时。 17* **优化状态管理,精准控制组件刷新范围**,在复用的场景下,需要控制状态变量的刷新范围,避免扩大刷新范围,降低组件复用的效率。 18* **复用组件嵌套结构会变更的场景,使用reuseId标记不同结构的组件构成**,如:使用if else结构来控制组件的创建,会造成组件树结构的大幅变动,降低组件复用的效率。需使用reuseId标记不同的组件结构,提升复用性能。 19* **不要使用函数/方法作为复用组件的入参**,复用时会触发组件的构造,如果函数入参中存在耗时操作,会影响复用性能。 20 21## 组件复用原理机制 22 23 24 251. 如上图①中,ListItem N-1滑出可视区域**即将销毁**时,如果标记了@Reusable,就会进入这个自定义组件**所在父组件**的复用缓存区。需注意**在自定义组件首次显示时,不会触发组件复用**。后续创建新组件节点时,会复用缓存区中的节点,节约组件重新创建的时间。尤其是该复用组件具有相同的布局结构,仅有某些数据差异时,通过组件复用可以提高列表页面的加载速度和响应速度。 26 272. 如上图②中,**复用缓存池是一个Map套Array的数据结构,以reuseId为key**,具有相同reuseId的组件在同一个Array中。如未设置reuseId,则reuseId默认是自定义组件的名字。 28 293. 如上图③中,发生复用行为时,会自动递归调用复用池中取出的自定义组件的aboutToReuse回调,应用可以在这个时候刷新数据。 30 31 32 33## 减少组件复用的嵌套层级 34 35在组件复用场景下,过深的自定义组件的嵌套会增加组件复用的使用难度,比如需要逐个实现所有嵌套组件中aboutToReuse回调实现数据更新;因此推荐优先使用@Builder替代自定义组件,减少嵌套层级,利于维护切能提升页面加载速度。正反例如下: 36 37反例: 38 39```ts 40@Entry 41@Component 42struct lessEmbeddedComponent { 43 aboutToAppear(): void { 44 getFriendMomentFromRawfile(); 45 } 46 47 build() { 48 Column() { 49 List({ space: ListConstants.LIST_SPACE }) { 50 LazyForEach(momentData, (moment: FriendMoment) => { 51 ListItem() { 52 OneMomentNoBuilder({moment: moment}) 53 } 54 }, (moment: FriendMoment) => moment.id) 55 } 56 .cachedCount(Constants.CACHED_COUNT) 57 } 58 } 59} 60 61@Reusable 62@Component 63export struct OneMomentNoBuilder { 64 @Prop moment: FriendMoment; 65 66 // 无需对@Prop修饰的变量进行aboutToReuse赋值,因为这些变量是由父组件传递给子组件的。如果在子组件中重新赋值这些变量,会导致重用的组件的内容重新触发状态刷新,从而降低组件的复用性能。 67 build() { 68 // ... 69 // 在复用组件中嵌套使用自定义组件 70 Row() { 71 InteractiveButton({ 72 imageStr: $r('app.media.ic_share'), 73 text: $r('app.string.friendMomentsPage_share') 74 }) 75 Blank() 76 InteractiveButton({ 77 imageStr: $r('app.media.ic_thumbsup'), 78 text: $r('app.string.friendMomentsPage_thumbsup') 79 }) 80 Blank() 81 InteractiveButton({ 82 imageStr: $r('app.media.ic_message'), 83 text: $r('app.string.friendMomentsPage_message') 84 }) 85 } 86 // ... 87 } 88} 89 90@Component 91export struct InteractiveButton { 92 @State imageStr: ResourceStr; 93 @State text: ResourceStr; 94 95 // 嵌套的组件中也需要实现aboutToReuse来进行UI的刷新 96 aboutToReuse(params: Record<string, Object>): void { 97 this.imageStr = params.imageStr as ResourceStr; 98 this.text = params.text as ResourceStr; 99 } 100 101 build() { 102 Row() { 103 Image(this.imageStr) 104 Text(this.text) 105 } 106 .alignItems(VerticalAlign.Center) 107 } 108} 109 110``` 111 112上述反例的操作中,在复用的自定义组件中嵌套了新的自定义组件。ArkUI中使用自定义组件时,在build阶段将在在后端FrameNode树创建一个相应的CustomNode节点,在渲染阶段时也会创建对应的RenderNode节点。会造成组件复用下,CustomNode创建和和RenderNod渲染的耗时。且嵌套的自定义组件InteractiveButton,也需要实现aboutToReuse来进行数据的刷新。 113 114优化前,以11号列表项复用过程为例,观察Trace信息,看到该过程中需要逐个实现所有嵌套组件InteractiveButton中aboutToReuse回调,导致复用时间较长,BuildLazyItem耗时7ms。 115 116 117 118正例: 119 120```ts 121@Entry 122@Component 123struct lessEmbeddedComponent { 124 aboutToAppear(): void { 125 getFriendMomentFromRawfile(); 126 } 127 128 build() { 129 Column() { 130 TopBar() 131 List({ space: ListConstants.LIST_SPACE }) { 132 LazyForEach(momentData, (moment: FriendMoment) => { 133 ListItem() { 134 OneMoment({moment: moment}) 135 } 136 }, (moment: FriendMoment) => moment.id) 137 } 138 .cachedCount(Constants.CACHED_COUNT) 139 } 140 } 141} 142 143@Reusable 144@Component 145export struct OneMoment { 146 @Prop moment: FriendMoment; 147 148 build() { 149 // ... 150 // 使用@Builder,可以减少自定义组件创建和渲染的耗时 151 Row() { 152 interactiveButton({ 153 imageStr: $r('app.media.ic_share'), 154 text: $r('app.string.friendMomentsPage_share') 155 }) 156 Blank() 157 interactiveButton({ 158 imageStr: $r('app.media.ic_thumbsup'), 159 text: $r('app.string.friendMomentsPage_thumbsup') 160 }) 161 Blank() 162 interactiveButton({ 163 imageStr: $r('app.media.ic_message'), 164 text: $r('app.string.friendMomentsPage_message') 165 }) 166 } 167 // ... 168 } 169} 170 171class Temp { 172 imageStr: ResourceStr = ''; 173 text: ResourceStr = ''; 174} 175 176@Builder 177export function interactiveButton($$: Temp) { 178 Row() { 179 // 此处使用$$来进行按引用传递,让@Builder感知到数据变化,进行UI刷新 180 Image($$.imageStr) 181 Text($$.text) 182 } 183} 184``` 185 186上述正例的操作中,在复用的自定义组件中用@Builder来代替了自定义组件。避免了CustomNode节点创建和RenderNode渲染的耗时。 187 188**优化效果** 189 190在正反例中,针对列表滑动场景中单个列表项中的三个交互按钮,反例中采用了自定义组件方式实现,正例中采用了自定义构建函数方式实现。 191 192优化后,11号列表项复用时,不再需要需要逐个实现所有嵌套组件中aboutToReuse回调,BuildLazyItem耗时3ms。可见该示例中,BuildLazyItem优化大约4ms。 193 194 195 196所以,Trace数据证明,优先使用@Builder替代自定义组件,减少嵌套层级,可以利于维护切能提升页面加载速度。 197 198## 优化状态管理,精准控制组件刷新范围使用 199 200### 使用AttributeUpdater精准控制组件属性的刷新,避免组件不必要的属性刷新 201 202复用场景常用在高频的刷新场景,精准控制组件的刷新范围可以有效减少主线程渲染负载,提升滑动性能。正反例如下: 203 204反例: 205 206```ts 207@Component 208export struct LessEmbeddedComponent { 209 aboutToAppear(): void { 210 momentData.getFriendMomentFromRawfile(); 211 } 212 213 build() { 214 Column() { 215 Text('use nothing') 216 List({ space: ListConstants.LIST_SPACE }) { 217 LazyForEach(momentData, (moment: FriendMoment) => { 218 ListItem() { 219 OneMomentNoModifier({ color: moment.color }) 220 .onClick(() => { 221 console.log(`my id is ${moment.id}`); 222 }) 223 } 224 }, (moment: FriendMoment) => moment.id) 225 } 226 .width("100%") 227 .height("100%") 228 .cachedCount(5) 229 } 230 } 231} 232 233@Reusable 234@Component 235export struct OneMomentNoModifier { 236 @State color: string | number | Resource = ""; 237 238 aboutToReuse(params: Record<string, Object>): void { 239 this.color = params.color as number; 240 } 241 242 build() { 243 Column() { 244 Text('这是标题') 245 Text('这是内部文字') 246 .fontColor(this.color)// 此处使用属性直接进行刷新,会造成Text所有属性都刷新 247 .textAlign(TextAlign.Center) 248 .fontStyle(FontStyle.Normal) 249 .fontSize(13) 250 .lineHeight(30) 251 .opacity(0.6) 252 .margin({ top: 10 }) 253 .fontWeight(30) 254 .clip(false) 255 .backgroundBlurStyle(BlurStyle.NONE) 256 .foregroundBlurStyle(BlurStyle.NONE) 257 .borderWidth(1) 258 .borderColor(Color.Pink) 259 .borderStyle(BorderStyle.Solid) 260 .alignRules({ 261 'top': { 'anchor': '__container__', 'align': VerticalAlign.Top }, 262 'left': { 'anchor': 'image', 'align': HorizontalAlign.End } 263 }) 264 } 265 } 266} 267``` 268 269上述反例的操作中,通过aboutToReuse对fontColor状态变量更新,进而导致组件的全部属性进行刷新,造成不必要的耗时。因此可以考虑对需要更新的组件的属性,进行精准刷新,避免不必要的重绘和渲染。 270 271 272 273优化前,由`H:ViewPU.viewPropertyHasChanged OneMomentNoModifier color 1`标签可知,OneMomentNoModifier自定义组件下的状态变量color发生变化,与之相关联的子控件数量为1,即有一个子控件发生了标脏,之后Text全部属性会进行了刷新。 274 275此时,`H:CustomNode:BuildRecycle`耗时543μs,`Create[Text]`耗时为4μs。 276 277 278 279 280 281正例: 282 283```typescript 284import { AttributeUpdater } from '@ohos.arkui.modifier'; 285 286export class MyTextUpdater extends AttributeUpdater<TextAttribute> { 287 private color: string | number | Resource = ""; 288 289 constructor(color: string | number | Resource) { 290 super(); 291 this.color = color; 292 } 293 294 initializeModifier(instance: TextAttribute): void { 295 instance.fontColor(this.color); // 差异化更新 296 } 297} 298 299@Component 300export struct UpdaterComponent { 301 aboutToAppear(): void { 302 momentData.getFriendMomentFromRawfile(); 303 } 304 305 build() { 306 Column() { 307 Text('use MyTextUpdater') 308 List({ space: ListConstants.LIST_SPACE }) { 309 LazyForEach(momentData, (moment: FriendMoment) => { 310 ListItem() { 311 OneMomentNoModifier({ color: moment.color }) 312 .onClick(() => { 313 console.log(`my id is ${moment.id}`); 314 }) 315 } 316 }, (moment: FriendMoment) => moment.id) 317 } 318 .cachedCount(5) 319 } 320 } 321} 322 323@Reusable 324@Component 325export struct OneMomentNoModifier { 326 color: string | number | Resource = ""; 327 textUpdater: MyTextUpdater | null = null; 328 329 aboutToAppear(): void { 330 this.textUpdater = new MyTextUpdater(this.color); 331 } 332 333 aboutToReuse(params: Record<string, Object>): void { 334 this.color = params.color as string; 335 this.textUpdater?.attribute?.fontColor(this.color); 336 } 337 338 build() { 339 Column() { 340 Text('这是标题') 341 Text('这是内部文字') 342 .attributeModifier(this.textUpdater) // 采用attributeUpdater来对需要更新的fontColor属性进行精准刷新,避免不必要的属性刷新。 343 .textAlign(TextAlign.Center) 344 .fontStyle(FontStyle.Normal) 345 .fontSize(13) 346 .lineHeight(30) 347 .opacity(0.6) 348 .margin({ top: 10 }) 349 .fontWeight(30) 350 .clip(false) 351 .backgroundBlurStyle(BlurStyle.NONE) 352 .foregroundBlurStyle(BlurStyle.NONE) 353 .borderWidth(1) 354 .borderColor(Color.Pink) 355 .borderStyle(BorderStyle.Solid) 356 .alignRules({ 357 'top': { 'anchor': '__container__', 'align': VerticalAlign.Top }, 358 'left': { 'anchor': 'image', 'align': HorizontalAlign.End } 359 }) 360 } 361 } 362} 363``` 364 365上述正例的操作中,通过AttributeUpdater来对Text组件需要刷新的属性进行精准刷新,避免Text其它不需要更改的属性的刷新。 366 367 368 369优化后,在`H:aboutToReuse`标签下没有`H:ViewPU.viewPropertyHasChanged`标签,后续也没有`Create[Text]`标签。此时,`H:CustomNode:BuildRecycle`耗时415μs。 370 371**优化效果** 372 373在正反例中,针对列表滑动场景中,单个列表项中Text组件字体颜色属性的修改,反例中采用了普通组件属性刷新方式实现,正例中采用了AttributeUpdater动态属性设置方式实现。 374 375优化后的`H:CustomNode:BuildRecycle OneMomentNoModifier`的耗时,如下表所示: 376 377| 次数 | 反例:使用@State(单位μs) | 正例:使用AttributeUpdater(单位μs) | 378| --- | --- | --- | 379| 1 | 357 | 338 | 380| 2 | 903 | 494 | 381| 3 | 543 | 415 | 382| 4 | 543 | 451 | 383| 5 | 692 | 509 | 384| 平均 | 607 | 441 | 385 386> 不同设备和场景都会对数据有影响,该数据仅供参考。 387 388所以,Trace数据证明,精准控制组件的刷新范围可以有效减少主线程渲染负载,提升滑动性能。 389 390> 因为示例中仅涉及一个Text组件的属性更新,所以优化时间绝对值较小。如果涉及组件较多,性能提升会更明显。 391 392### 使用@Link/@ObjectLink替代@Prop减少深拷贝,提升组件创建速度 393 394在父子组件数据同步时,如果仅仅是需要父组件向子组件同步数据,不存在修改子组件的数据变化不同步给父组件的需求。建议使用@Link/@ObjectLink替代@Prop,@Prop在装饰变量时会进行深拷贝,在拷贝的过程中除了基本类型、Map、Set、Date、Array外,都会丢失类型。正反例如下: 395 396反例: 397 398```ts 399@Entry 400@Component 401struct lessEmbeddedComponent { 402 aboutToAppear(): void { 403 getFriendMomentFromRawfile(); 404 } 405 406 build() { 407 Column() { 408 TopBar() 409 List({ space: ListConstants.LIST_SPACE }) { 410 LazyForEach(momentData, (moment: FriendMoment) => { 411 ListItem() { 412 OneMoment({moment: moment}) 413 } 414 }, (moment: FriendMoment) => moment.id) 415 } 416 .cachedCount(Constants.CACHED_COUNT) 417 } 418 } 419} 420 421@Reusable 422@Component 423export struct OneMoment { 424 @Prop moment: FriendMoment; 425 426 build() { 427 Column() { 428 // ... 429 Text(`${this.moment.userName}`) 430 // ... 431 } 432 } 433} 434 435export const momentData: FriendMomentsData = new FriendMomentsData(); 436 437export class FriendMoment { 438 id: string; 439 userName: string; 440 avatar: string; 441 text: string; 442 size: number; 443 image?: string; 444 445 constructor(id: string, userName: string, avatar: string, text: string, size: number, image?: string) { 446 this.id = id; 447 this.userName = userName; 448 this.avatar = avatar; 449 this.text = text; 450 this.size = size; 451 if (image !== undefined) { 452 this.image = image; 453 } 454 } 455} 456``` 457 458上述反例的操作中,父子组件之间的数据同步用了@Prop来进行,各@Prop装饰的变量在初始化时都在本地拷贝了一份数据。会增加创建时间及内存的消耗,造成性能问题。 459 460优化前,子组件在初始化时都在本地拷贝了一份数据,BuildItem耗时7ms175μs。 461 462 463 464正例: 465 466```ts 467@Entry 468@Component 469struct lessEmbeddedComponent { 470 @State momentData: FriendMomentsData = new FriendMomentsData(); 471 aboutToAppear(): void { 472 getFriendMomentFromRawfile(); 473 } 474 475 build() { 476 Column() { 477 TopBar() 478 List({ space: ListConstants.LIST_SPACE }) { 479 LazyForEach(momentData, (moment: FriendMoment) => { 480 ListItem() { 481 OneMoment({moment: moment}) 482 } 483 }, (moment: FriendMoment) => moment.id) 484 } 485 .cachedCount(Constants.CACHED_COUNT) 486 } 487 } 488} 489 490@Reusable 491@Component 492export struct OneMoment { 493 @ObjectLink moment: FriendMoment; 494 495 build() { 496 Column() { 497 // ... 498 Text(`${this.moment.userName}`) 499 // ... 500 } 501 } 502} 503 504@Observed 505export class FriendMoment { 506 id: string; 507 userName: string; 508 avatar: string; 509 text: string; 510 size: number; 511 image?: string; 512 513 constructor(id: string, userName: string, avatar: string, text: string, size: number, image?: string) { 514 this.id = id; 515 this.userName = userName; 516 this.avatar = avatar; 517 this.text = text; 518 this.size = size; 519 if (image !== undefined) { 520 this.image = image; 521 } 522 } 523} 524``` 525 526上述正例的操作中,父子组件之间的数据同步用了@ObjectLink来进行,子组件@ObjectLink包装类把当前this指针注册给父组件,会直接将父组件的数据同步给子组件,实现父子组件数据的双向同步,降低子组件创建时间和内存消耗。 527 528**优化效果** 529 530在正反例中,针对列表滑动场景,反例采用@Prop修饰的变量,来进行父子组件间的数据同步。子组件在初始化时@Prop修饰的变量,都在本地拷贝了一份数据,增加了组件创建的时间;正例采用@ObjectLink来进行父子组件间的数据同步,把当前this指针注册给父组件,减少了组件创建的时间。 531 532优化后,子组件直接同步父组件数据,无需深拷贝,BuildItem耗时缩短为7ms1μs。 533 534 535 536所以,Trace数据证明,使用@Link/@ObjectLink替代@Prop减少深拷贝,可以提升组件创建速度。 537 538> **说明:** 539> 540> 因为示例中仅涉及一个简单对象FriendMoment的深拷贝,所以优化时间绝对值较小。如果涉及变量较多、对象较复杂,性能提升会更明显。 541 542### 避免对@Link/@ObjectLink/@Prop等自动更新的状态变量,在aboutToReuse方法中再进行更新 543 544在父子组件数据同步时,如果子组件已经使用@Link/@ObjectLink/@Prop等会自动同步父子组件数据、且驱动组件刷新的状态变量。不需要再在boutToReuse方法中再进行数据更新,此操作会造成不必要的方法执行和变量更新的耗时。正反例如下: 545 546反例: 547 548```ts 549@Entry 550@Component 551struct LessEmbeddedComponent { 552 @State momentData: FriendMomentsData = new FriendMomentsData(); 553 aboutToAppear(): void { 554 getFriendMomentFromRawfile(); 555 } 556 557 build() { 558 Column() { 559 TopBar() 560 List({ space: ListConstants.LIST_SPACE }) { 561 LazyForEach(momentData, (moment: FriendMoment) => { 562 ListItem() { 563 OneMoment({moment: moment}) 564 } 565 }, (moment: FriendMoment) => moment.id) 566 } 567 .cachedCount(Constants.CACHED_COUNT) 568 } 569 } 570} 571 572@Reusable 573@Component 574export struct OneMoment { 575 // 该类型的状态变量已包含自动刷新功能,不需要再重复进行刷新 576 @ObjectLink moment: FriendMoment; 577 578 // 此处aboutToReuse为多余刷新 579 aboutToReuse(params: Record<string, Object>): void { 580 this.moment.id = (params.moment as FriendMoment).id; 581 this.moment.userName = (params.moment as FriendMoment).userName; 582 this.moment.avatar = (params.moment as FriendMoment).avatar; 583 this.moment.text = (params.moment as FriendMoment).text; 584 this.moment.image = (params.moment as FriendMoment).image; 585 } 586 587 build() { 588 Column() { 589 // ... 590 Text(`${this.moment.userName}`) 591 // ... 592 } 593 } 594} 595 596@Observed 597export class FriendMoment { 598 id: string; 599 userName: string; 600 avatar: string; 601 text: string; 602 size: number; 603 image?: string; 604 605 constructor(id: string, userName: string, avatar: string, text: string, size: number, image?: string) { 606 this.id = id; 607 this.userName = userName; 608 this.avatar = avatar; 609 this.text = text; 610 this.size = size; 611 if (image !== undefined) { 612 this.image = image; 613 } 614 } 615} 616``` 617 618上述反例的操作中,子组件中moment变量被@ObjectLink修饰,把当前this指针注册给父组件,会直接将父组件的数据同步给子组件,实现数据刷新。重新在aboutToReuse中刷新,如果刷新涉及的变量较多、变量中成员变量复杂,可能会造成较大性能开销。 619 620优化前,由于在复用组件OneMoment的aboutToReuse方法中,对moment变量的各个成员变量进行了刷新,aboutToReuse耗时168μs。 621 622 623 624正例: 625 626```ts 627@Entry 628@Component 629struct LessEmbeddedComponent { 630 @State momentData: FriendMomentsData = new FriendMomentsData(); 631 aboutToAppear(): void { 632 getFriendMomentFromRawfile(); 633 } 634 635 build() { 636 Column() { 637 TopBar() 638 List({ space: ListConstants.LIST_SPACE }) { 639 LazyForEach(momentData, (moment: FriendMoment) => { 640 ListItem() { 641 OneMoment({moment: moment}) 642 } 643 }, (moment: FriendMoment) => moment.id) 644 } 645 .cachedCount(Constants.CACHED_COUNT) 646 } 647 } 648} 649 650@Reusable 651@Component 652export struct OneMoment { 653 @ObjectLink moment: FriendMoment; 654 655 build() { 656 Column() { 657 // ... 658 Text(`${this.moment.userName}`) 659 // ... 660 } 661 } 662} 663 664@Observed 665export class FriendMoment { 666 id: string; 667 userName: string; 668 avatar: string; 669 text: string; 670 size: number; 671 image?: string; 672 673 constructor(id: string, userName: string, avatar: string, text: string, size: number, image?: string) { 674 this.id = id; 675 this.userName = userName; 676 this.avatar = avatar; 677 this.text = text; 678 this.size = size; 679 if (image !== undefined) { 680 this.image = image; 681 } 682 } 683} 684``` 685 686上述正例的操作中,子组件中moment变量被@ObjectLink修饰,把当前this指针注册给父组件,会直接将父组件的数据同步给子组件,实现数据刷新。 687 688**优化效果** 689 690在正反例中,针对列表滑动场景,反例中在aboutToReuse方法中,冗余刷新了自动刷新的变量moment中的各个成员变量。正例中,利用@ObjectLink修饰的变量moment自动同步数据的特性,直接进行刷新,不在aboutToReuse方法再进行刷新。 691 692优化后,避免在复用组件OneMoment的aboutToReuse方法中,重复刷新变量moment的各个成员变量,aboutToReuse耗时110μs。 693 694 695 696所以,通过上述Trace数据证明,避免在复用组件中,对@Link/@ObjectLink/@Prop等自动更新的状态变量,在aboutToReuse方法中再进行更新。会减少aboutToReuse方法的时间,进而减少复用组件的创建时间。 697 698> **说明:** 699> 700> 因为示例中仅涉及一个简单变量moment的各成员变量的冗余刷新,所以优化时间绝对值不大。如果涉及变量较多、变量中成员变量复杂,性能提升会更明显。 701 702## 复用组件嵌套结构会变更的场景,使用reuseId标记不同结构的组件构成 703 704在自定义组件复用的场景中,如果使用if/else条件语句来控制布局的结构,会导致在不同逻辑创建不同布局结构嵌套的组件,从而造成组件树结构的不同。此时我们应该使用reuseId来区分不同结构的组件,确保系统能够根据reuseId缓存各种结构的组件,提升复用性能。正反例如下: 705 706反例: 707 708```ts 709@Entry 710@Component 711struct withoutReuseId { 712 aboutToAppear(): void { 713 getFriendMomentFromRawfile(); 714 } 715 716 build() { 717 Column() { 718 TopBar() 719 List({ space: ListConstants.LIST_SPACE }) { 720 LazyForEach(momentData, (moment: FriendMoment) => { 721 ListItem() { 722 // 此处的复用组件,只有一个reuseId,为组件的名称。但是该复用组件中又存在if else重新创建组件的逻辑 723 TrueOneMoment({ moment: moment, sum: this.sum, fontSize: moment.size }) 724 } 725 }, (moment: FriendMoment) => moment.id) 726 } 727 .cachedCount(Constants.CACHED_COUNT) 728 } 729 } 730} 731 732@Reusable 733@Component 734export struct TrueOneMoment { 735 @Prop moment: FriendMoment; 736 @State sum: number = 0; 737 @State fontSize: number | Resource = $r('app.integer.list_history_userText_fontSize'); 738 739 aboutToReuse(params: ESObject): void { 740 this.fontSize = params.fontSize as number; 741 this.sum = params.sum as number; 742 } 743 744 build() { 745 Column() { 746 if (this.moment.image) { 747 FalseOneMoment({ moment: this.moment, sum: this.sum, fontSize: this.moment.size }) 748 } else { 749 OneMoment({ moment: this.moment, sum: this.sum, fontSize: this.moment.size }) 750 } 751 } 752 .width('100%') 753 } 754} 755``` 756 757上述反例的操作中,在一个reuseId标识的组件TrueOneMoment中,通过if来控制其中的组件走不同的分支,选择是否创建FalseOneMoment或OneMoment组件。导致更新if分支时仍然可能走删除重创的逻辑(此处BuildItem重新创建了OneMoment组件)。考虑采用根据不同的分支设置不同的reuseId来提高复用的性能。 758 759优化前,15号列表项复用时长为10ms左右,且存在自定义组件创建的情况。 760 761 762 763正例: 764 765```ts 766@Entry 767@Component 768struct withoutReuseId { 769 aboutToAppear(): void { 770 getFriendMomentFromRawfile(); 771 } 772 773 build() { 774 Column() { 775 TopBar() 776 List({ space: ListConstants.LIST_SPACE }) { 777 LazyForEach(momentData, (moment: FriendMoment) => { 778 ListItem() { 779 // 使用不同的reuseId标记,保证TrueOneMoment中各个子组件在复用时,不重新创建 780 TrueOneMoment({ moment: moment, sum: this.sum, fontSize: moment.size }) 781 .reuseId((moment.image !=='' ?'withImage' : 'noImage')) 782 } 783 }, (moment: FriendMoment) => moment.id) 784 } 785 .cachedCount(Constants.CACHED_COUNT) 786 } 787 } 788} 789 790@Reusable 791@Component 792export struct TrueOneMoment { 793 @Prop moment: FriendMoment; 794 @State sum: number = 0; 795 @State fontSize: number | Resource = $r('app.integer.list_history_userText_fontSize'); 796 797 aboutToReuse(params: ESObject): void { 798 this.fontSize = params.fontSize as number; 799 this.sum = params.sum as number; 800 } 801 802 build() { 803 Column() { 804 if (this.moment.image) { 805 FalseOneMoment({ moment: this.moment, sum: this.sum, fontSize: this.moment.size }) 806 } else { 807 OneMoment({ moment: this.moment, sum: this.sum, fontSize: this.moment.size }) 808 } 809 } 810 .width('100%') 811 } 812} 813``` 814 815上述正例的操作中,通过不同的reuseId来标识需要复用的组件,省去走if删除重创的逻辑,提高组件复用的效率和性能。 816 817**优化效果** 818 819针对列表滑动场景中,复用的组件中又存在多个自定义组件。通过if进行条件渲染,存在不同逻辑创建不同布局结构的组件的情况。反例中多个复用组件使用相同的复用标识reuseId,正例中采用不同的复用标识reuseId区分不同结构的自定义组件。 820 821优化后,15号列表项复用时长缩短为3ms左右,不存在自定义组件的创建。 822 823 824 825所以,Trace数据证明,针对不同逻辑创建不同布局结构嵌套的组件的情况,通过使用reuseId来区分不同结构的组件,能减少删除重创的逻辑,提高组件复用的效率和性能。 826 827## 避免使用函数/方法作为复用组件创建时的入参 828 829由于在组件复用的场景下,每次复用都需要重新创建组件关联的数据对象,导致重复执行入参中的函数来获取入参结果。如果函数中存在耗时操作,会严重影响性能。正反例如下: 830 831反例: 832 833```ts 834@Entry 835@Component 836struct withFuncParam { 837 aboutToAppear(): void { 838 getFriendMomentFromRawfile(); 839 } 840 // 真实场景的函数中可能存在未知的耗时操作逻辑,此处用循环函数模拟耗时操作 841 countAndReturn(): number { 842 let temp: number = 0; 843 for (let index = 0; index < 100000; index++) { 844 temp += index; 845 } 846 return temp; 847 } 848 849 build() { 850 Column() { 851 TopBar() 852 List({ space: ListConstants.LIST_SPACE }) { 853 LazyForEach(momentData, (moment: FriendMoment) => { 854 ListItem() { 855 OneMoment({ 856 moment: moment, 857 sum: this.countAndReturn() 858 }) 859 } 860 }, (moment: FriendMoment) => moment.id) 861 } 862 .cachedCount(Constants.CACHED_COUNT) 863 } 864 } 865} 866 867@Reusable 868@Component 869export struct OneMoment { 870 @Prop moment: FriendMoment; 871 @State sum: number = 0; 872 873 aboutToReuse(params: Record<string, Object>): void { 874 this.sum = params.sum as number; 875 } 876 877 build() { 878 Column() { 879 // ... 880 Text(`${this.moment.userName} (${this.moment.id} / ${this.sum})`) 881 // ... 882 } 883 } 884} 885``` 886 887上述反例的操作中,复用的子组件参数sum是通过耗时函数生成。该函数在每次组件复用时都需要执行,会造成性能问题,甚至是列表滑动过程中的卡顿丢帧现象。 888 889优化前,aboutToReuse中需要重复执行入参中的函数来获取入参结果,导致耗时较长为4ms。 890 891 892 893正例: 894 895```ts 896@Entry 897@Component 898struct withFuncParam { 899 @State sum: number = 0; 900 901 aboutToAppear(): void { 902 getFriendMomentFromRawfile(); 903 // 执行该异步函数 904 this.countAndRecord(); 905 } 906 // 真实场景的函数中可能存在未知的耗时操作逻辑,此处用循环函数模拟耗时操作 907 async countAndRecord() { 908 let temp: number = 0; 909 for (let index = 0; index < 100000; index++) { 910 temp += index; 911 } 912 // 将结果放入状态变量中 913 this.sum = temp; 914 } 915 916 build() { 917 Column() { 918 TopBar() 919 List({ space: ListConstants.LIST_SPACE }) { 920 LazyForEach(momentData, (moment: FriendMoment) => { 921 ListItem() { 922 // 子组件的传参通过状态变量进行 923 OneMoment({ 924 moment: moment, 925 sum: this.sum 926 }) 927 } 928 }, (moment: FriendMoment) => moment.id) 929 } 930 .cachedCount(Constants.CACHED_COUNT) 931 } 932 } 933} 934 935@Reusable 936@Component 937export struct OneMoment { 938 @Prop moment: FriendMoment; 939 @State sum: number = 0; 940 941 aboutToReuse(params: Record<string, Object>): void { 942 this.sum = params.sum as number; 943 } 944 945 build() { 946 Column() { 947 // ... 948 Text(`${this.moment.userName} (${this.moment.id} / ${this.sum})`) 949 // ... 950 } 951 } 952} 953``` 954 955上述正例的操作中,通过耗时函数countAndRecord生成的结果不变,可以将其放到页面初始渲染时执行一次,将结果赋值给this.sum。在复用组件的参数传递时,通过this.sum来进行。 956 957**优化效果** 958 959针对列表滑动场景,单个列表项中的一个Text组件,需要依赖复用组件创建时的入参,反例中入参直接传入函数,正例中入参通过状态变量传递。 960 961优化后,aboutToReuse中只是通过变量传参,无需重复执行计算函数,耗时缩短为2ms。 962 963 964 965所以,Trace数据证明,避免使用函数/方法作为复用组件创建时的入参,可以减少重复执行入参中的函数所带来的性能消耗。 966