• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![组件复用机制图](./figures/component_recycle_case.png)
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![noBuilder](./figures/component_recycle_case/noBuilder.png)
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![useBuilder](./figures/component_recycle_case/useBuilder.png)
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![noModifier1](./figures/component_recycle_case/noModifier1.png)
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![noModifier2](./figures/component_recycle_case/noModifier2.png)
278
279![noModifier3](./figures/component_recycle_case/noModifier3.png)
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![useUpdater1](./figures/component_recycle_case/useUpdater1.png)
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![useProp](./figures/component_recycle_case/useProp.png)
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![useLink](./figures/component_recycle_case/useLink.png)
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![refresh_auto_fresh_variable](./figures/component_recycle_case/avoid_auto_variable_false_trace.png)
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![aovid_refresh_auto_fresh_variable](./figures/component_recycle_case/avoid_auto_vaiable_true_trace.png)
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![noReuseId](./figures/component_recycle_case/noReuseId.png)
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![ReuseId](./figures/component_recycle_case/ReuseId.png)
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![FuncParam](./figures/component_recycle_case/FuncParam.png)
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![noFuncParam](./figures/component_recycle_case/noFuncParam.png)
964
965所以,Trace数据证明,避免使用函数/方法作为复用组件创建时的入参,可以减少重复执行入参中的函数所带来的性能消耗。
966