1#  组件复用性能优化指导
2
3## 概述
4
5在滑动场景下,常常会对同一类自定义组件的实例进行频繁的创建与销毁。此时可以考虑通过组件复用减少频繁创建与销毁的能耗。组件复用时,可能存在许多影响组件复用效率的操作,本篇文章将重点介绍如何通过**组件复用性能优化四建议**提升复用性能。
6
7组件复用性能优化四建议:
8
9* **减少组件复用的嵌套层级**,如果在复用的自定义组件中再嵌套自定义组件,会存在节点构造的开销,且需要在每个嵌套的子组件中的aboutToReuse方法中实现数据的刷新,造成耗时。
10* **优化状态管理,精准控制组件刷新范围**,在复用的场景下,需要控制状态变量的刷新范围,避免扩大刷新范围,降低组件复用的效率。
11* **复用组件嵌套结构会变更的场景,使用reuseId标记不同结构的组件构成**,如:使用if else结构来控制组件的创建,会造成组件树结构的大幅变动,降低组件复用的效率。需使用reuseId标记不同的组件结构,提升复用性能。
12* **不要使用函数/方法作为复用组件的入参**,复用时会触发组件的构造,如果函数入参中存在耗时操作,会影响复用性能。
13
14## 组件复用原理机制
15
16![组件复用机制图](./figures/component_recycle_case.png)
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![noBuilder](./figures/component_recycle_case/noBuilder.png)
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![useBuilder](./figures/component_recycle_case/useBuilder.png)
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![noModifier1](./figures/component_recycle_case/noModifier1.png)
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![noModifier2](./figures/component_recycle_case/noModifier2.png)
271
272![noModifier3](./figures/component_recycle_case/noModifier3.png)
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![useUpdater1](./figures/component_recycle_case/useUpdater1.png)
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![useProp](./figures/component_recycle_case/useProp.png)
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![useLink](./figures/component_recycle_case/useLink.png)
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![refresh_auto_fresh_variable](./figures/component_recycle_case/avoid_auto_variable_false_trace.png)
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![aovid_refresh_auto_fresh_variable](./figures/component_recycle_case/avoid_auto_vaiable_true_trace.png)
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![noReuseId](./figures/component_recycle_case/noReuseId.png)
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![ReuseId](./figures/component_recycle_case/ReuseId.png)
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![FuncParam](./figures/component_recycle_case/FuncParam.png)
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![noFuncParam](./figures/component_recycle_case/noFuncParam.png)
957
958所以,Trace数据证明,避免使用函数/方法作为复用组件创建时的入参,可以减少重复执行入参中的函数所带来的性能消耗。
959