• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Freezing a Custom Component
2
3Freezing a custom component is designed to optimize the performance of complex UI pages, especially for scenarios where multiple page stacks, long lists, or grid layouts are involved. In these cases, when the state variable is bound to multiple UI components, the change of the state variables may trigger the re-render of a large number of UI components, resulting in frame freezing and response delay. To improve the UI re-render performance, you can try to use the custom component freezing function.
4
5Principles of freezing a component are as follows:
61. Setting the **freezeWhenInactive** attribute to activate the component freezing mechanism.
72. After this function is enabled, the system re-renders only the activated custom components. In this way, the UI framework can narrow down the re-render scope to the (activated) custom components that are visible to users, improving the re-render efficiency in complex UI scenarios.
83. When an inactive custom component turns into the active state, the state management framework performs necessary re-render operations on the custom component to ensure that the UI is correctly displayed.
9
10In short, component freezing aims to optimize UI re-render performance on complex UIs. When there are multiple invisible custom components, such as multiple page stacks, long lists, or grids, you can freeze the components to re-render visible custom components as required, and the re-render of the invisible custom components is delayed until they become visible.
11
12Note that the active or inactive state of a component is not equivalent to its visibility. Component freezing applies only to the following scenarios:
13
141. Page routing: The current top page of the navigation stack is in the active state, and the non-top invisible page is in the inactive state.
152. TabContent: Only the custom component in the currently displayed TabContent is in the active state.
163. LazyForEach: Only the custom component in the currently displayed LazyForEach is in the active state, and the component of the cache node is in the inactive state.
174. Navigation: Only the custom component in the currently displayed NavDestination is in the active state.
185. Component reuse: The component that enters the reuse pool is in the inactive state, and the node attached from the reuse pool is in the active state.
19In other scenarios, for example, masked components in a stack layout are not considered to be in an inactive state although they are invisible. Therefore, component freezing cannot be applied to these components.
20
21Before reading this topic, you are advised to read [Creating a Custom Component](./arkts-create-custom-components.md) to learn about the basic syntax.
22
23> **NOTE**
24>
25> Custom component freezing is supported since API version 11.
26
27## Use Scenarios
28
29### Page Routing
30
31> **NOTE**
32>
33> This example uses router for page redirection but you are advised to use the **Navigation** component instead, because **Navigation** provides more functions and more flexible customization capabilities. For details, see the use cases of [Navigation](#navigation).
34
35When page 1 calls the **router.pushUrl** API to jump to page 2, page 1 is hidden and invisible. In this case, if the state variable on page 1 is updated, page 1 is not re-rendered.
36For details, see the following.
37
38![freezeInPage](./figures/freezeInPage.png)
39
40Page 1
41
42```ts
43import { router } from '@kit.ArkUI';
44
45@Entry
46@Component({ freezeWhenInactive: true })
47struct Page1 {
48  @StorageLink('PropA') @Watch("first") storageLink: number = 47;
49
50  first() {
51    console.info("first page " + `${this.storageLink}`)
52  }
53
54  build() {
55    Column() {
56      Text(`From first Page ${this.storageLink}`).fontSize(50)
57      Button('first page storageLink + 1').fontSize(30)
58        .onClick(() => {
59          this.storageLink += 1
60        })
61      Button('go to next page').fontSize(30)
62        .onClick(() => {
63          router.pushUrl({ url: 'pages/Page2' })
64        })
65    }
66  }
67}
68```
69
70Page 2
71
72```ts
73import { router } from '@kit.ArkUI';
74
75@Entry
76@Component({ freezeWhenInactive: true })
77struct Page2 {
78  @StorageLink('PropA') @Watch("second") storageLink2: number = 1;
79
80  second() {
81    console.info("second page: " + `${this.storageLink2}`)
82  }
83
84  build() {
85    Column() {
86
87      Text(`second Page ${this.storageLink2}`).fontSize(50)
88      Button('Change Divider.strokeWidth')
89        .onClick(() => {
90          router.back()
91        })
92
93      Button('second page storageLink2 + 2').fontSize(30)
94        .onClick(() => {
95          this.storageLink2 += 2
96        })
97
98    }
99  }
100}
101```
102
103In the preceding example:
104
1051. When the button **first page storageLink + 1** on page 1 is clicked, the **storageLink** state variable is updated, and the @Watch decorated **first** method is called.
106
1072. Through **router.pushUrl({url:'pages/second'})**, page 2 is displayed, and page 1 is hidden with its state changing from active to inactive.
108
1093. When the button **this.storageLink2 += 2** on page 2 is clicked, only the @Watch decorated **second** method of page 2 is called, because page 1 has been frozen when inactive.
110
1114. When the **back** button is clicked, page 2 is destroyed, and page 1 changes from inactive to active. At this time, if the state variable of page 1 is updated, the @Watch decorated **first** method of page 1 is called again.
112
113
114### TabContent
115
116- You can freeze invisible **TabContent** components in the **Tabs** container so that they do not trigger UI re-rendering.
117
118- During initial rendering, only the **TabContent** component that is being displayed is created. All **TabContent** components are created only after all of them have been switched to.
119
120For details, see the following.
121![freezeWithTab](./figures/freezewithTabs.png)
122
123```ts
124@Entry
125@Component
126struct TabContentTest {
127  @State @Watch("onMessageUpdated") message: number = 0;
128  private data: number[] = [0, 1]
129
130  onMessageUpdated() {
131    console.info(`TabContent message callback func ${this.message}`)
132  }
133
134  build() {
135    Row() {
136      Column() {
137        Button('change message').onClick(() => {
138          this.message++
139        })
140
141        Tabs() {
142          ForEach(this.data, (item: number) => {
143            TabContent() {
144              FreezeChild({ message: this.message, index: item })
145            }.tabBar(`tab${item}`)
146          }, (item: number) => item.toString())
147        }
148      }
149      .width('100%')
150    }
151    .height('100%')
152  }
153}
154
155@Component({ freezeWhenInactive: true })
156struct FreezeChild {
157  @Link @Watch("onMessageUpdated") message: number
158  private index: number = 0
159
160  onMessageUpdated() {
161    console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`)
162  }
163
164  build() {
165    Text("message" + `${this.message}, index: ${this.index}`)
166      .fontSize(50)
167      .fontWeight(FontWeight.Bold)
168  }
169}
170```
171
172In the preceding example:
173
1741. When **change message** is clicked, the value of **message** changes, and the @Watch decorated **onMessageUpdated** method of the **TabContent** component being displayed is called.
175
1762. When you click **two** to switch to another **TabContent** component, it switches from inactive to active, and the corresponding @Watch decorated **onMessageUpdated** method is called.
177
1783. When **change message** is clicked again, the value of **message** changes, and only the @Watch decorated **onMessageUpdated** method of the **TabContent** component being displayed is called.
179
180![TabContent.gif](figures/TabContent.gif)
181
182
183### LazyForEach
184
185- You can freeze custom components cached in **LazyForEach** so that they do not trigger UI re-rendering.
186
187```ts
188// Basic implementation of IDataSource used to listening for data.
189class BasicDataSource implements IDataSource {
190  private listeners: DataChangeListener[] = [];
191  private originDataArray: string[] = [];
192
193  public totalCount(): number {
194    return 0;
195  }
196
197  public getData(index: number): string {
198    return this.originDataArray[index];
199  }
200
201  // This method is called by the framework to add a listener to the LazyForEach data source.
202  registerDataChangeListener(listener: DataChangeListener): void {
203    if (this.listeners.indexOf(listener) < 0) {
204      console.info('add listener');
205      this.listeners.push(listener);
206    }
207  }
208
209  // This method is called by the framework to remove the listener from the LazyForEach data source.
210  unregisterDataChangeListener(listener: DataChangeListener): void {
211    const pos = this.listeners.indexOf(listener);
212    if (pos >= 0) {
213      console.info('remove listener');
214      this.listeners.splice(pos, 1);
215    }
216  }
217
218  // Notify LazyForEach that all child components need to be reloaded.
219  notifyDataReload(): void {
220    this.listeners.forEach(listener => {
221      listener.onDataReloaded();
222    })
223  }
224
225  // Notify LazyForEach that a child component needs to be added for the data item with the specified index.
226  notifyDataAdd(index: number): void {
227    this.listeners.forEach(listener => {
228      listener.onDataAdd(index);
229    })
230  }
231
232  // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt.
233  notifyDataChange(index: number): void {
234    this.listeners.forEach(listener => {
235      listener.onDataChange(index);
236    })
237  }
238
239  // Notify LazyForEach that the child component that matches the specified index needs to be deleted.
240  notifyDataDelete(index: number): void {
241    this.listeners.forEach(listener => {
242      listener.onDataDelete(index);
243    })
244  }
245}
246
247class MyDataSource extends BasicDataSource {
248  private dataArray: string[] = [];
249
250  public totalCount(): number {
251    return this.dataArray.length;
252  }
253
254  public getData(index: number): string {
255    return this.dataArray[index];
256  }
257
258  public addData(index: number, data: string): void {
259    this.dataArray.splice(index, 0, data);
260    this.notifyDataAdd(index);
261  }
262
263  public pushData(data: string): void {
264    this.dataArray.push(data);
265    this.notifyDataAdd(this.dataArray.length - 1);
266  }
267}
268
269@Entry
270@Component
271struct LforEachTest {
272  private data: MyDataSource = new MyDataSource();
273  @State @Watch("onMessageUpdated") message: number = 0;
274
275  onMessageUpdated() {
276    console.info(`LazyforEach message callback func ${this.message}`)
277  }
278
279  aboutToAppear() {
280    for (let i = 0; i <= 20; i++) {
281      this.data.pushData(`Hello ${i}`)
282    }
283  }
284
285  build() {
286    Column() {
287      Button('change message').onClick(() => {
288        this.message++
289      })
290      List({ space: 3 }) {
291        LazyForEach(this.data, (item: string) => {
292          ListItem() {
293            FreezeChild({ message: this.message, index: item })
294          }
295        }, (item: string) => item)
296      }.cachedCount(5).height(500)
297    }
298
299  }
300}
301
302@Component({ freezeWhenInactive: true })
303struct FreezeChild {
304  @Link @Watch("onMessageUpdated") message: number;
305  private index: string = "";
306
307  aboutToAppear() {
308    console.info(`FreezeChild aboutToAppear index: ${this.index}`)
309  }
310
311  onMessageUpdated() {
312    console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`)
313  }
314
315  build() {
316    Text("message" + `${this.message}, index: ${this.index}`)
317      .width('90%')
318      .height(160)
319      .backgroundColor(0xAFEEEE)
320      .textAlign(TextAlign.Center)
321      .fontSize(30)
322      .fontWeight(FontWeight.Bold)
323  }
324}
325```
326
327In the preceding example:
328
3291. When **change message** is clicked, the value of **message** changes, the @Watch decorated **onMessageUpdated** method of the list items being displayed is called, and that of the cached list items is not called. (If the component is not frozen, the @Watch decorated **onMessageUpdated** method of both list items that are being displayed and cached list items is called.)
330
3312. When a list item moves from outside the list content area into the list content area, it switches from inactive to active, and the corresponding @Watch decorated **onMessageUpdated** method is called.
332
3333. When **change message** is clicked again, the value of **message** changes, and only the @Watch decorated **onMessageUpdated** method of the list items being displayed is called.
334
335![FrezzeLazyforEach.gif](figures/FrezzeLazyforEach.gif)
336
337### Navigation
338
339- When the navigation destination page is invisible, its child custom components are set to the inactive state and will not be re-rendered. When return to this page, its child custom components are restored to the active state and the @Watch callback is triggered to re-render the page.
340
341- In the following example, **NavigationContentMsgStack** is set to the inactive state, which does not respond to the change of the state variables, and does not trigger component re-rendering.
342
343```ts
344@Entry
345@Component
346struct MyNavigationTestStack {
347  @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack();
348  @State @Watch("info") message: number = 0;
349  @State logNumber: number = 0;
350
351  info() {
352    console.info(`freeze-test MyNavigation message callback ${this.message}`);
353  }
354
355  @Builder
356  PageMap(name: string) {
357    if (name === 'pageOne') {
358      pageOneStack({ message: this.message, logNumber: this.logNumber })
359    } else if (name === 'pageTwo') {
360      pageTwoStack({ message: this.message, logNumber: this.logNumber })
361    } else if (name === 'pageThree') {
362      pageThreeStack({ message: this.message, logNumber: this.logNumber })
363    }
364  }
365
366  build() {
367    Column() {
368      Button('change message')
369        .onClick(() => {
370          this.message++;
371        })
372      Navigation(this.pageInfo) {
373        Column() {
374          Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
375            .width('80%')
376            .height(40)
377            .margin(20)
378            .onClick(() => {
379              this.pageInfo.pushPath({ name: 'pageOne' }); // Push the navigation destination page specified by name to the navigation stack.
380            })
381        }
382      }.title('NavIndex')
383      .navDestination(this.PageMap)
384      .mode(NavigationMode.Stack)
385    }
386  }
387}
388
389@Component
390struct pageOneStack {
391  @Consume('pageInfo') pageInfo: NavPathStack;
392  @State index: number = 1;
393  @Link message: number;
394  @Link logNumber: number;
395
396  build() {
397    NavDestination() {
398      Column() {
399        NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber })
400        Text("cur stack size:" + `${this.pageInfo.size()}`)
401          .fontSize(30)
402          .fontWeight(FontWeight.Bold)
403        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
404          .width('80%')
405          .height(40)
406          .margin(20)
407          .onClick(() => {
408            this.pageInfo.pushPathByName('pageTwo', null);
409          })
410        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
411          .width('80%')
412          .height(40)
413          .margin(20)
414          .onClick(() => {
415            this.pageInfo.pop();
416          })
417      }.width('100%').height('100%')
418    }.title('pageOne')
419    .onBackPressed(() => {
420      this.pageInfo.pop();
421      return true;
422    })
423  }
424}
425
426@Component
427struct pageTwoStack {
428  @Consume('pageInfo') pageInfo: NavPathStack;
429  @State index: number = 2;
430  @Link message: number;
431  @Link logNumber: number;
432
433  build() {
434    NavDestination() {
435      Column() {
436        NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber })
437        Text("cur stack size:" + `${this.pageInfo.size()}`)
438          .fontSize(30)
439          .fontWeight(FontWeight.Bold)
440        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
441          .width('80%')
442          .height(40)
443          .margin(20)
444          .onClick(() => {
445            this.pageInfo.pushPathByName('pageThree', null);
446          })
447        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
448          .width('80%')
449          .height(40)
450          .margin(20)
451          .onClick(() => {
452            this.pageInfo.pop();
453          })
454      }.width('100%').height('100%')
455    }.title('pageTwo')
456    .onBackPressed(() => {
457      this.pageInfo.pop();
458      return true;
459    })
460  }
461}
462
463@Component
464struct pageThreeStack {
465  @Consume('pageInfo') pageInfo: NavPathStack;
466  @State index: number = 3;
467  @Link message: number;
468  @Link logNumber: number;
469
470  build() {
471    NavDestination() {
472      Column() {
473        NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber })
474        Text("cur stack size:" + `${this.pageInfo.size()}`)
475          .fontSize(30)
476          .fontWeight(FontWeight.Bold)
477        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
478          .width('80%')
479          .height(40)
480          .margin(20)
481          .onClick(() => {
482            this.pageInfo.pushPathByName('pageOne', null);
483          })
484        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
485          .width('80%')
486          .height(40)
487          .margin(20)
488          .onClick(() => {
489            this.pageInfo.pop();
490          })
491      }.width('100%').height('100%')
492    }.title('pageThree')
493    .onBackPressed(() => {
494      this.pageInfo.pop();
495      return true;
496    })
497  }
498}
499
500@Component({ freezeWhenInactive: true })
501struct NavigationContentMsgStack {
502  @Link @Watch("info") message: number;
503  @Link index: number;
504  @Link logNumber: number;
505
506  info() {
507    console.info(`freeze-test NavigationContent message callback ${this.message}`);
508    console.info(`freeze-test ---- called by content ${this.index}`);
509    this.logNumber++;
510  }
511
512  build() {
513    Column() {
514      Text("msg:" + `${this.message}`)
515        .fontSize(30)
516        .fontWeight(FontWeight.Bold)
517      Text("log number:" + `${this.logNumber}`)
518        .fontSize(30)
519        .fontWeight(FontWeight.Bold)
520    }
521  }
522}
523```
524
525In the preceding example:
526
5271. When **change message** is clicked, the value of **message** changes, and the @Watch decorated **info** method of the **MyNavigationTestStack** component being displayed is called.
528
5292. When **Next Page** is clicked, **PageOne** is displayed, and the **PageOneStack** node is created.
530
5313. When **change message** is clicked again, the value of **message** changes, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageOneStack** is called.
532
5334. When **Next Page** is clicked again, **PageTwo** is displayed, and the **pageTwoStack** node is created.
534
5355. When **change message** is clicked again, the value of **message** changes, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageTwoStack** is called.
536
5376. When **Next Page** is clicked again, **PageThree** is displayed, and the **pageThreeStack** node is created.
538
5397. When **change message** is clicked again, the value of **message** changes, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageThreeStack** is called.
540
5418. When **Back Page** is clicked, **PageTwo** is displayed, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageTwoStack** is called.
542
5439. When **Back Page** is clicked again, **PageOne** is displayed, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageOneStack** is called.
544
54510. When **Back Page** is clicked again, the initial page is displayed, and no method is called.
546
547![navigation-freeze.gif](figures/navigation-freeze.gif)
548
549### Reusing Components
550
551<!--RP1-->[Components reuse](../performance/component-recycle.md)<!--RP1End--> existing nodes in the cache pool instead of creating new nodes to optimize UI performance and improve application smoothness. Although the nodes in the reuse pool are not displayed in the UI component tree, the change of the state variable still triggers the UI re-render. To solve the problem that components in the reuse pool are re-rendered abnormally, you can perform component freezing.
552
553#### Mixed Use of Component Reuse, if, and Component Freezing
554The following example shows that when the state variable bound to the **if** component changes to **false**, the detach of **ChildComponent** is triggered. Because **ChildComponent** is marked as component reuse, it is not destroyed but enters the reuse pool, in this case, if the component freezing is enabled at the same time, the component will not be re-rendered in the reuse pool.
555The procedure is as follows:
5561. Click **change flag** and change the value of **flag** to **false**.
557    -  When **ChildComponent** marked with \@Reusable is detached, it is not destroyed. Instead, it enters the reuse pool, triggers the **aboutToRecycle** lifecycle, and sets the component state to inactive.
558    - **ChildComponent** also enables component freezing. When **ChildComponent** is in the inactive state, it does not respond to any UI re-render caused by state variable changes.
5592. Click **change desc** to trigger the change of the member variable **desc** of **Page**.
560    - The change of \@State decorated **desc** will be notified to \@Link decorated **desc** of **ChildComponent**.
561    - However, **ChildComponent** is in the inactive state and the component freezing is enabled. Therefore, the change does not trigger the callback of @Watch('descChange') and the re-render of the `ChildComponent` UI. If component freezing is not enabled, the current @Watch('descChange') callback is returned immediately, and **ChildComponent** in the reuse pool is re-rendered accordingly.
5623. Click **change flag** again and change the value of **flag** to **true**.
563    - **ChildComponent** is attached to the component tree from the reuse pool.
564    - Return the **aboutToReuse** lifecycle callback and synchronize the latest **count** value to **ChildComponent**. The value of **desc** is synchronized from @State to @Link. Therefore, you do not need to manually assign a value to **aboutToReuse**.
565    - Set **ChildComponent** to the active state and re-render the component that is not re-rendered when **ChildComponent** is inactive, for example, **Text (ChildComponent desc: ${this.desc})**.
566
567
568```ts
569@Reusable
570@Component({freezeWhenInactive: true})
571struct ChildComponent {
572  @Link @Watch('descChange') desc: string;
573  @State count: number = 0;
574  descChange() {
575    console.info(`ChildComponent messageChange ${this.desc}`);
576  }
577
578  aboutToReuse(params: Record<string, ESObject>): void {
579    this.count = params.count as number;
580  }
581
582  aboutToRecycle(): void {
583    console.info(`ChildComponent has been recycled`);
584  }
585  build() {
586    Column() {
587      Text(`ChildComponent desc: ${this.desc}`)
588        .fontSize(20)
589      Text(`ChildComponent count ${this.count}`)
590        .fontSize(20)
591    }.border({width: 2, color: Color.Pink})
592  }
593}
594
595@Entry
596@Component
597struct Page {
598  @State desc: string = 'Hello World';
599  @State flag: boolean = true;
600  @State count: number = 0;
601  build() {
602    Column() {
603      Button(`change desc`).onClick(() => {
604        this.desc += '!';
605      })
606      Button(`change flag`).onClick(() => {
607        this.count++;
608        this.flag =! this.flag;
609      })
610      if (this.flag) {
611        ChildComponent({desc: this.desc, count: this.count})
612      }
613    }
614    .height('100%')
615  }
616}
617```
618#### Mixed Use of LazyForEach, Component Reuse, and Component Freezing
619In the scrolling scenario of a long list with a large amount of data, you can use **LazyForEach** to create components as required. In addition, you can reuse components to reduce the overhead caused by component creation and destruction during scrolling.
620However, if you set <!--RP2-->[reuseId](../performance/component-recycle.md#available-apis)<!--RP2End--> based on the reuse type or assign a large value to **cacheCount** to ensure the scrolling performance, more nodes will be cached in the reuse pool or **LazyForEach**.
621In this case, if you trigger the re-render of all subnodes in **List**, the number of re-renders is too large. In this case, you can freeze the component.
622
623Example:
6241. Swipe the list to the position whose index is 14. There are 15 **ChildComponent** in the visible area on the current page.
6252. During swiping:
626    - **ChildComponent** in the upper part of the list is swiped out of the visible area. In this case, **ChildComponent** enters the cache area of LazyForEach and is set to inactive. After the component slides out of the **LazyForEach** area, the component is not destructed and enters the reuse pool because the component is marked for reuse. In this case, the component is set to inactive again.
627    - The cache node of **LazyForEach** at the bottom of the list enters the list. In this case, the system attempts to create a node to enter the cache of **LazyForEach**. If a node that can be reused is found, the system takes out the existing node from the reuse pool and triggers the **aboutToReuse** lifecycle callback, in this case, the node enters the cache area of **LazyForEach** and the state of the node is still inactive.
6283. Click **change desc** to trigger the change of the member variable **desc** of **Page**.
629    - The change of \@State decorated **desc** will be notified to \@Link decorated **desc** of **ChildComponent**.
630    - **ChildComponent** in the invisible area is in the inactive state, and the component freezing is enabled. Therefore, this change triggers the @Watch('descChange') callback of the 15 nodes in the visible area and re-renders these nodes. Nodes cached in **LazyForEach** and the reuse pool are not re-rendered, and the \@Watch callback is not triggered.
631
632
633For details, see the following.
634![freeze](./figures/freezeResuable.png)
635You can listen for the changes by \@Trace, only 15 **ChildComponent** nodes are re-rendered.
636![freeze](./figures/traceWithFreeze.png)
637A complete sample code is as follows:
638```ts
639import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
640// Basic implementation of IDataSource used to listening for data.
641class BasicDataSource implements IDataSource {
642  private listeners: DataChangeListener[] = [];
643  private originDataArray: string[] = [];
644
645  public totalCount(): number {
646    return 0;
647  }
648
649  public getData(index: number): string {
650    return this.originDataArray[index];
651  }
652
653  // This method is called by the framework to add a listener to the LazyForEach data source.
654  registerDataChangeListener(listener: DataChangeListener): void {
655    if (this.listeners.indexOf(listener) < 0) {
656      console.info('add listener');
657      this.listeners.push(listener);
658    }
659  }
660
661  // This method is called by the framework to remove the listener from the LazyForEach data source.
662  unregisterDataChangeListener(listener: DataChangeListener): void {
663    const pos = this.listeners.indexOf(listener);
664    if (pos >= 0) {
665      console.info('remove listener');
666      this.listeners.splice(pos, 1);
667    }
668  }
669
670  // Notify LazyForEach that all child components need to be reloaded.
671  notifyDataReload(): void {
672    this.listeners.forEach(listener => {
673      listener.onDataReloaded();
674    })
675  }
676
677  // Notify LazyForEach that a child component needs to be added for the data item with the specified index.
678  notifyDataAdd(index: number): void {
679    this.listeners.forEach(listener => {
680      listener.onDataAdd(index);
681    })
682  }
683
684  // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt.
685  notifyDataChange(index: number): void {
686    this.listeners.forEach(listener => {
687      listener.onDataChange(index);
688    })
689  }
690
691  // Notify LazyForEach that the child component that matches the specified index needs to be deleted.
692  notifyDataDelete(index: number): void {
693    this.listeners.forEach(listener => {
694      listener.onDataDelete(index);
695    })
696  }
697
698  // Notify LazyForEach that data needs to be swapped between the from and to positions.
699  notifyDataMove(from: number, to: number): void {
700    this.listeners.forEach(listener => {
701      listener.onDataMove(from, to);
702    })
703  }
704}
705
706class MyDataSource extends BasicDataSource {
707  private dataArray: string[] = [];
708
709  public totalCount(): number {
710    return this.dataArray.length;
711  }
712
713  public getData(index: number): string {
714    return this.dataArray[index];
715  }
716
717  public addData(index: number, data: string): void {
718    this.dataArray.splice(index, 0, data);
719    this.notifyDataAdd(index);
720  }
721
722  public pushData(data: string): void {
723    this.dataArray.push(data);
724    this.notifyDataAdd(this.dataArray.length - 1);
725  }
726}
727
728@Reusable
729@Component({freezeWhenInactive: true})
730struct ChildComponent {
731  @Link @Watch('descChange') desc: string;
732  @State item: string = '';
733  @State index: number = 0;
734  descChange() {
735    console.info(`ChildComponent messageChange ${this.desc}`);
736  }
737
738  aboutToReuse(params: Record<string, ESObject>): void {
739    this.item = params.item;
740    this.index = params.index;
741  }
742
743  aboutToRecycle(): void {
744    console.info(`ChildComponent has been recycled`);
745  }
746  build() {
747    Column() {
748      Text(`ChildComponent index: ${this.index} item: ${this.item}`)
749        .fontSize(20)
750      Text(`desc: ${this.desc}`)
751        .fontSize(20)
752    }.border({width: 2, color: Color.Pink})
753  }
754}
755
756@Entry
757@Component
758struct Page {
759  @State desc: string = 'Hello World';
760  private data: MyDataSource = new MyDataSource();
761
762  aboutToAppear() {
763    for (let i = 0; i < 50; i++) {
764      this.data.pushData(`Hello ${i}`);
765    }
766  }
767
768  build() {
769    Column() {
770      Button(`change desc`).onClick(() => {
771        hiTraceMeter.startTrace('change decs', 1);
772        this.desc += '!';
773        hiTraceMeter.finishTrace('change decs', 1);
774      })
775      List({ space: 3 }) {
776        LazyForEach(this.data, (item: string, index: number) => {
777          ListItem() {
778            ChildComponent({index: index, item: item, desc: this.desc}).reuseId(index % 10 < 5 ? "1": "0")
779          }
780        }, (item: string) => item)
781      }.cachedCount(5)
782    }
783    .height('100%')
784  }
785}
786```
787#### Mixed Use of LazyForEach, if, Component Reuse, and Component Freezing
788
789 Under the same parent custom component, reusable nodes may enter the reuse pool in different ways. For example:
790- Detaching from the cache area of LazyForEach by swiping.
791- Notifying the subnodes to detach by switching the if condition.
792
793In the following example:
7941. When you swipe the list to the position whose index is 14, there are 10 **ChildComponent**s in the visible area on the page, among which nine are subnodes of **LazyForEach** and one is a subnode of **if**.
7952. Click **change flag**. The **if** condition is changed to **false**, and its subnode **ChildComponent** enters the reuse pool. Nine nodes are displayed on the page.
7963. In this case, the nodes detached through **LazyForEach** or **if** all enter the reuse pool under the **Page** node.
7974. Click **change desc** to update only the nine **ChildComponent** nodes on the page. For details, see figures below.
7985. Click **change flag** again. The **if** condition changes to **true**, and **ChildComponent** is attached from the reuse pool to the component tree again. The state of **ChildComponent** changes to active.
7996. Click **change desc** again. The nodes attached through **if** and **LazyForEach** from the reuse pool can be re-rendered.
800
801Trace for component freezing enabled
802
803![traceWithFreezeLazyForeachAndIf](./figures/traceWithFreezeLazyForeachAndIf.png)
804
805Trace for component freezing disabled
806
807![traceWithFreezeLazyForeachAndIf](./figures/traceWithLazyForeachAndIf.png)
808
809
810A complete example is as follows:
811```
812import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
813class BasicDataSource implements IDataSource {
814  private listeners: DataChangeListener[] = [];
815  private originDataArray: string[] = [];
816
817  public totalCount(): number {
818    return 0;
819  }
820
821  public getData(index: number): string {
822    return this.originDataArray[index];
823  }
824
825  // This method is called by the framework to add a listener to the LazyForEach data source.
826  registerDataChangeListener(listener: DataChangeListener): void {
827    if (this.listeners.indexOf(listener) < 0) {
828      console.info('add listener');
829      this.listeners.push(listener);
830    }
831  }
832
833  // This method is called by the framework to remove the listener from the LazyForEach data source.
834  unregisterDataChangeListener(listener: DataChangeListener): void {
835    const pos = this.listeners.indexOf(listener);
836    if (pos >= 0) {
837      console.info('remove listener');
838      this.listeners.splice(pos, 1);
839    }
840  }
841
842  // Notify LazyForEach that all child components need to be reloaded.
843  notifyDataReload(): void {
844    this.listeners.forEach(listener => {
845      listener.onDataReloaded();
846    })
847  }
848
849  // Notify LazyForEach that a child component needs to be added for the data item with the specified index.
850  notifyDataAdd(index: number): void {
851    this.listeners.forEach(listener => {
852      listener.onDataAdd(index);
853    })
854  }
855
856  // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt.
857  notifyDataChange(index: number): void {
858    this.listeners.forEach(listener => {
859      listener.onDataChange(index);
860    })
861  }
862
863  // Notify LazyForEach that the child component that matches the specified index needs to be deleted.
864  notifyDataDelete(index: number): void {
865    this.listeners.forEach(listener => {
866      listener.onDataDelete(index);
867    })
868  }
869
870  // Notify LazyForEach that data needs to be swapped between the from and to positions.
871  notifyDataMove(from: number, to: number): void {
872    this.listeners.forEach(listener => {
873      listener.onDataMove(from, to);
874    })
875  }
876}
877
878class MyDataSource extends BasicDataSource {
879  private dataArray: string[] = [];
880
881  public totalCount(): number {
882    return this.dataArray.length;
883  }
884
885  public getData(index: number): string {
886    return this.dataArray[index];
887  }
888
889  public addData(index: number, data: string): void {
890    this.dataArray.splice(index, 0, data);
891    this.notifyDataAdd(index);
892  }
893
894  public pushData(data: string): void {
895    this.dataArray.push(data);
896    this.notifyDataAdd(this.dataArray.length - 1);
897  }
898}
899
900@Reusable
901@Component({freezeWhenInactive: true})
902struct ChildComponent {
903  @Link @Watch('descChange') desc: string;
904  @State item: string = '';
905  @State index: number = 0;
906  descChange() {
907    console.info(`ChildComponent messageChange ${this.desc}`);
908  }
909
910  aboutToReuse(params: Record<string, ESObject>): void {
911    this.item = params.item;
912    this.index = params.index;
913  }
914
915  aboutToRecycle(): void {
916    console.info(`ChildComponent has been recycled`);
917  }
918  build() {
919    Column() {
920      Text(`ChildComponent index: ${this.index} item: ${this.item}`)
921        .fontSize(20)
922      Text(`desc: ${this.desc}`)
923        .fontSize(20)
924    }.border({width: 2, color: Color.Pink})
925  }
926}
927
928@Entry
929@Component
930struct Page {
931  @State desc: string = 'Hello World';
932  @State flag: boolean = true;
933  private data: MyDataSource = new MyDataSource();
934
935  aboutToAppear() {
936    for (let i = 0; i < 50; i++) {
937      this.data.pushData(`Hello ${i}`);
938    }
939  }
940
941  build() {
942    Column() {
943      Button(`change desc`).onClick(() => {
944        hiTraceMeter.startTrace('change decs', 1);
945        this.desc += '!';
946        hiTraceMeter.finishTrace('change decs', 1);
947      })
948
949      Button(`change flag`).onClick(() => {
950        hiTraceMeter.startTrace('change flag', 1);
951        this.flag = !this.flag;
952        hiTraceMeter.finishTrace('change flag', 1);
953      })
954
955      List({ space: 3 }) {
956        LazyForEach(this.data, (item: string, index: number) => {
957          ListItem() {
958            ChildComponent({index: index, item: item, desc: this.desc}).reuseId(index % 10 < 5 ? "1": "0")
959          }
960        }, (item: string) => item)
961      }
962      .cachedCount(5)
963      .height('60%')
964
965      if (this.flag) {
966        ChildComponent({index: -1, item: 'Hello', desc: this.desc}).reuseId( "1")
967      }
968    }
969    .height('100%')
970  }
971}
972```
973
974## Constraints
975
976As shown in the following example, the custom node [BuilderNode](../reference/apis-arkui/js-apis-arkui-builderNode.md) is used in **FreezeBuildNode**. **BuilderNode** can dynamically mount components using commands and component freezing strongly depends on the parent-child relationship to determine whether it is enabled. In this case, if the parent component is frozen and **BuilderNode** is enabled at the middle level of the component tree, the child component of the **BuilderNode** cannot be frozen.
977
978```
979import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI';
980
981// Define a Params class to pass parameters.
982class Params {
983  index: number = 0;
984
985  constructor(index: number) {
986    this.index = index;
987  }
988}
989
990// Define a buildNodeChild component that contains a message attribute and an index attribute.
991@Component
992struct buildNodeChild {
993  @StorageProp("buildNodeTest") @Watch("onMessageUpdated") message: string = "hello world";
994  @State index: number = 0;
995
996  // Call this method when message is updated.
997  onMessageUpdated() {
998    console.log(`FreezeBuildNode builderNodeChild message callback func ${this.message},index: ${this.index}`);
999  }
1000
1001  build() {
1002    Text(`buildNode Child message: ${this.message}`).fontSize(30)
1003  }
1004}
1005
1006// Define a buildText function that receives a Params parameter and constructs a Column component.
1007@Builder
1008function buildText(params: Params) {
1009  Column() {
1010    buildNodeChild({ index: params.index })
1011  }
1012}
1013
1014// Define a TextNodeController class that is inherited from NodeController.
1015class TextNodeController extends NodeController {
1016  private textNode: BuilderNode<[Params]> | null = null;
1017  private index: number = 0;
1018
1019  // The constructor receives an index parameter.
1020  constructor(index: number) {
1021    super();
1022    this.index = index;
1023  }
1024
1025  // Create and return a FrameNode.
1026  makeNode(context: UIContext): FrameNode | null {
1027    this.textNode = new BuilderNode(context);
1028    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.index));
1029    return this.textNode.getFrameNode();
1030  }
1031}
1032
1033// Define an index component that contains a message attribute and a data array.
1034@Entry
1035@Component
1036struct Index {
1037  @StorageLink("buildNodeTest") message: string = "hello";
1038  private data: number[] = [0, 1];
1039
1040  build() {
1041    Row() {
1042      Column() {
1043        Button("change").fontSize(30)
1044          .onClick(() => {
1045            this.message += 'a';
1046          })
1047
1048        Tabs() {
1049          ForEach(this.data, (item: number) => {
1050            TabContent() {
1051              FreezeBuildNode({ index: item })
1052            }.tabBar(`tab${item}`)
1053          }, (item: number) => item.toString())
1054        }
1055      }
1056    }
1057    .width('100%')
1058    .height('100%')
1059  }
1060}
1061
1062// Define a FreezeBuildNode component that contains a message attribute and an index attribute.
1063@Component({ freezeWhenInactive: true })
1064struct FreezeBuildNode {
1065  @StorageProp("buildNodeTest") @Watch("onMessageUpdated") message: string = "1111";
1066  @State index: number = 0;
1067
1068  // Call this method when message is updated.
1069  onMessageUpdated() {
1070    console.log(`FreezeBuildNode message callback func ${this.message}, index: ${this.index}`);
1071  }
1072
1073  build() {
1074    NodeContainer(new TextNodeController(this.index))
1075      .width('100%')
1076      .height('100%')
1077      .backgroundColor('#FFF0F0F0')
1078  }
1079}
1080```
1081
1082In the preceding example:
1083
1084Click **Button("change")** to change the value of **message**. The **onMessageUpdated** method registered in @Watch of the **TabContent** component that is being displayed is triggered, and that under the **BuilderNode** node of **TabContent** that is not displayed is also triggered.
1085
1086![builderNode.gif](figures/builderNode.gif)
1087