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