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.
19
20
21
22Before reading this topic, you are advised to read [Creating a Custom Component](./arkts-create-custom-components.md) to learn about the basic syntax.
23
24> **NOTE**
25>
26> Custom component freezing is supported since API version 11.
27
28## Use Scenarios
29
30### Page Routing
31
32> **NOTE**
33>
34> 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).
35
36When 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.
37For details, see the following.
38
39![freezeInPage](./figures/freezeInPage.png)
40
41Page 1
42
43```ts
44import { router } from '@kit.ArkUI';
45
46@Entry
47@Component({ freezeWhenInactive: true })
48struct Page1 {
49  @StorageLink('PropA') @Watch("first") storageLink: number = 47;
50
51  first() {
52    console.info("first page " + `${this.storageLink}`)
53  }
54
55  build() {
56    Column() {
57      Text(`From first Page ${this.storageLink}`).fontSize(50)
58      Button('first page storageLink + 1').fontSize(30)
59        .onClick(() => {
60          this.storageLink += 1
61        })
62      Button('go to next page').fontSize(30)
63        .onClick(() => {
64          router.pushUrl({ url: 'pages/Page2' })
65        })
66    }
67  }
68}
69```
70
71Page 2
72
73```ts
74import { router } from '@kit.ArkUI';
75
76@Entry
77@Component({ freezeWhenInactive: true })
78struct Page2 {
79  @StorageLink('PropA') @Watch("second") storageLink2: number = 1;
80
81  second() {
82    console.info("second page: " + `${this.storageLink2}`)
83  }
84
85  build() {
86    Column() {
87
88      Text(`second Page ${this.storageLink2}`).fontSize(50)
89      Button('Change Divider.strokeWidth')
90        .onClick(() => {
91          router.back()
92        })
93
94      Button('second page storageLink2 + 2').fontSize(30)
95        .onClick(() => {
96          this.storageLink2 += 2
97        })
98
99    }
100  }
101}
102```
103
104In the preceding example:
105
1061. 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.
107
1082. Through **router.pushUrl({url:'pages/second'})**, page 2 is displayed, and page 1 is hidden with its state changing from active to inactive.
109
1103. 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.
111
1124. 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.
113
114
115### TabContent
116
117- You can freeze invisible **TabContent** components in the **Tabs** container so that they do not trigger UI re-rendering.
118
119- 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.
120
121For details, see the following.
122![freezeWithTab](./figures/freezewithTabs.png)
123
124```ts
125@Entry
126@Component
127struct TabContentTest {
128  @State @Watch("onMessageUpdated") message: number = 0;
129  private data: number[] = [0, 1]
130
131  onMessageUpdated() {
132    console.info(`TabContent message callback func ${this.message}`)
133  }
134
135  build() {
136    Row() {
137      Column() {
138        Button('change message').onClick(() => {
139          this.message++
140        })
141
142        Tabs() {
143          ForEach(this.data, (item: number) => {
144            TabContent() {
145              FreezeChild({ message: this.message, index: item })
146            }.tabBar(`tab${item}`)
147          }, (item: number) => item.toString())
148        }
149      }
150      .width('100%')
151    }
152    .height('100%')
153  }
154}
155
156@Component({ freezeWhenInactive: true })
157struct FreezeChild {
158  @Link @Watch("onMessageUpdated") message: number
159  private index: number = 0
160
161  onMessageUpdated() {
162    console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`)
163  }
164
165  build() {
166    Text("message" + `${this.message}, index: ${this.index}`)
167      .fontSize(50)
168      .fontWeight(FontWeight.Bold)
169  }
170}
171```
172
173In the preceding example:
174
1751. When **change message** is clicked, the value of **message** changes, and the @Watch decorated **onMessageUpdated** method of the **TabContent** component being displayed is called.
176
1772. 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.
178
1793. 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.
180
181![TabContent.gif](figures/TabContent.gif)
182
183
184### LazyForEach
185
186- You can freeze custom components cached in **LazyForEach** so that they do not trigger UI re-rendering.
187
188```ts
189// Basic implementation of IDataSource used to listening for data.
190class BasicDataSource implements IDataSource {
191  private listeners: DataChangeListener[] = [];
192  private originDataArray: string[] = [];
193
194  public totalCount(): number {
195    return 0;
196  }
197
198  public getData(index: number): string {
199    return this.originDataArray[index];
200  }
201
202  // This method is called by the framework to add a listener to the LazyForEach data source.
203  registerDataChangeListener(listener: DataChangeListener): void {
204    if (this.listeners.indexOf(listener) < 0) {
205      console.info('add listener');
206      this.listeners.push(listener);
207    }
208  }
209
210  // This method is called by the framework to remove the listener from the LazyForEach data source.
211  unregisterDataChangeListener(listener: DataChangeListener): void {
212    const pos = this.listeners.indexOf(listener);
213    if (pos >= 0) {
214      console.info('remove listener');
215      this.listeners.splice(pos, 1);
216    }
217  }
218
219  // Notify LazyForEach that all child components need to be reloaded.
220  notifyDataReload(): void {
221    this.listeners.forEach(listener => {
222      listener.onDataReloaded();
223    })
224  }
225
226  // Notify LazyForEach that a child component needs to be added for the data item with the specified index.
227  notifyDataAdd(index: number): void {
228    this.listeners.forEach(listener => {
229      listener.onDataAdd(index);
230    })
231  }
232
233  // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt.
234  notifyDataChange(index: number): void {
235    this.listeners.forEach(listener => {
236      listener.onDataChange(index);
237    })
238  }
239
240  // Notify LazyForEach that the child component that matches the specified index needs to be deleted.
241  notifyDataDelete(index: number): void {
242    this.listeners.forEach(listener => {
243      listener.onDataDelete(index);
244    })
245  }
246}
247
248class MyDataSource extends BasicDataSource {
249  private dataArray: string[] = [];
250
251  public totalCount(): number {
252    return this.dataArray.length;
253  }
254
255  public getData(index: number): string {
256    return this.dataArray[index];
257  }
258
259  public addData(index: number, data: string): void {
260    this.dataArray.splice(index, 0, data);
261    this.notifyDataAdd(index);
262  }
263
264  public pushData(data: string): void {
265    this.dataArray.push(data);
266    this.notifyDataAdd(this.dataArray.length - 1);
267  }
268}
269
270@Entry
271@Component
272struct LforEachTest {
273  private data: MyDataSource = new MyDataSource();
274  @State @Watch("onMessageUpdated") message: number = 0;
275
276  onMessageUpdated() {
277    console.info(`LazyforEach message callback func ${this.message}`)
278  }
279
280  aboutToAppear() {
281    for (let i = 0; i <= 20; i++) {
282      this.data.pushData(`Hello ${i}`)
283    }
284  }
285
286  build() {
287    Column() {
288      Button('change message').onClick(() => {
289        this.message++
290      })
291      List({ space: 3 }) {
292        LazyForEach(this.data, (item: string) => {
293          ListItem() {
294            FreezeChild({ message: this.message, index: item })
295          }
296        }, (item: string) => item)
297      }.cachedCount(5).height(500)
298    }
299
300  }
301}
302
303@Component({ freezeWhenInactive: true })
304struct FreezeChild {
305  @Link @Watch("onMessageUpdated") message: number;
306  private index: string = "";
307
308  aboutToAppear() {
309    console.info(`FreezeChild aboutToAppear index: ${this.index}`)
310  }
311
312  onMessageUpdated() {
313    console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`)
314  }
315
316  build() {
317    Text("message" + `${this.message}, index: ${this.index}`)
318      .width('90%')
319      .height(160)
320      .backgroundColor(0xAFEEEE)
321      .textAlign(TextAlign.Center)
322      .fontSize(30)
323      .fontWeight(FontWeight.Bold)
324  }
325}
326```
327
328In the preceding example:
329
3301. 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.)
331
3322. 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.
333
3343. 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.
335
336![FrezzeLazyforEach.gif](figures/FrezzeLazyforEach.gif)
337
338### Navigation
339
340- 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.
341
342- 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.
343
344```ts
345@Entry
346@Component
347struct MyNavigationTestStack {
348  @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack();
349  @State @Watch("info") message: number = 0;
350  @State logNumber: number = 0;
351
352  info() {
353    console.info(`freeze-test MyNavigation message callback ${this.message}`);
354  }
355
356  @Builder
357  PageMap(name: string) {
358    if (name === 'pageOne') {
359      pageOneStack({ message: this.message, logNumber: this.logNumber })
360    } else if (name === 'pageTwo') {
361      pageTwoStack({ message: this.message, logNumber: this.logNumber })
362    } else if (name === 'pageThree') {
363      pageThreeStack({ message: this.message, logNumber: this.logNumber })
364    }
365  }
366
367  build() {
368    Column() {
369      Button('change message')
370        .onClick(() => {
371          this.message++;
372        })
373      Navigation(this.pageInfo) {
374        Column() {
375          Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
376            .width('80%')
377            .height(40)
378            .margin(20)
379            .onClick(() => {
380              this.pageInfo.pushPath({ name: 'pageOne' }); // Push the navigation destination page specified by name to the navigation stack.
381            })
382        }
383      }.title('NavIndex')
384      .navDestination(this.PageMap)
385      .mode(NavigationMode.Stack)
386    }
387  }
388}
389
390@Component
391struct pageOneStack {
392  @Consume('pageInfo') pageInfo: NavPathStack;
393  @State index: number = 1;
394  @Link message: number;
395  @Link logNumber: number;
396
397  build() {
398    NavDestination() {
399      Column() {
400        NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber })
401        Text("cur stack size:" + `${this.pageInfo.size()}`)
402          .fontSize(30)
403          .fontWeight(FontWeight.Bold)
404        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
405          .width('80%')
406          .height(40)
407          .margin(20)
408          .onClick(() => {
409            this.pageInfo.pushPathByName('pageTwo', null);
410          })
411        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
412          .width('80%')
413          .height(40)
414          .margin(20)
415          .onClick(() => {
416            this.pageInfo.pop();
417          })
418      }.width('100%').height('100%')
419    }.title('pageOne')
420    .onBackPressed(() => {
421      this.pageInfo.pop();
422      return true;
423    })
424  }
425}
426
427@Component
428struct pageTwoStack {
429  @Consume('pageInfo') pageInfo: NavPathStack;
430  @State index: number = 2;
431  @Link message: number;
432  @Link logNumber: number;
433
434  build() {
435    NavDestination() {
436      Column() {
437        NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber })
438        Text("cur stack size:" + `${this.pageInfo.size()}`)
439          .fontSize(30)
440          .fontWeight(FontWeight.Bold)
441        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
442          .width('80%')
443          .height(40)
444          .margin(20)
445          .onClick(() => {
446            this.pageInfo.pushPathByName('pageThree', null);
447          })
448        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
449          .width('80%')
450          .height(40)
451          .margin(20)
452          .onClick(() => {
453            this.pageInfo.pop();
454          })
455      }.width('100%').height('100%')
456    }.title('pageTwo')
457    .onBackPressed(() => {
458      this.pageInfo.pop();
459      return true;
460    })
461  }
462}
463
464@Component
465struct pageThreeStack {
466  @Consume('pageInfo') pageInfo: NavPathStack;
467  @State index: number = 3;
468  @Link message: number;
469  @Link logNumber: number;
470
471  build() {
472    NavDestination() {
473      Column() {
474        NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber })
475        Text("cur stack size:" + `${this.pageInfo.size()}`)
476          .fontSize(30)
477          .fontWeight(FontWeight.Bold)
478        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
479          .width('80%')
480          .height(40)
481          .margin(20)
482          .onClick(() => {
483            this.pageInfo.pushPathByName('pageOne', null);
484          })
485        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
486          .width('80%')
487          .height(40)
488          .margin(20)
489          .onClick(() => {
490            this.pageInfo.pop();
491          })
492      }.width('100%').height('100%')
493    }.title('pageThree')
494    .onBackPressed(() => {
495      this.pageInfo.pop();
496      return true;
497    })
498  }
499}
500
501@Component({ freezeWhenInactive: true })
502struct NavigationContentMsgStack {
503  @Link @Watch("info") message: number;
504  @Link index: number;
505  @Link logNumber: number;
506
507  info() {
508    console.info(`freeze-test NavigationContent message callback ${this.message}`);
509    console.info(`freeze-test ---- called by content ${this.index}`);
510    this.logNumber++;
511  }
512
513  build() {
514    Column() {
515      Text("msg:" + `${this.message}`)
516        .fontSize(30)
517        .fontWeight(FontWeight.Bold)
518      Text("log number:" + `${this.logNumber}`)
519        .fontSize(30)
520        .fontWeight(FontWeight.Bold)
521    }
522  }
523}
524```
525
526In the preceding example:
527
5281. When **change message** is clicked, the value of **message** changes, and the @Watch decorated **info** method of the **MyNavigationTestStack** component being displayed is called.
529
5302. When **Next Page** is clicked, **PageOne** is displayed, and the **PageOneStack** node is created.
531
5323. 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.
533
5344. When **Next Page** is clicked again, **PageTwo** is displayed, and the **pageTwoStack** node is created.
535
5365. 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.
537
5386. When **Next Page** is clicked again, **PageThree** is displayed, and the **pageThreeStack** node is created.
539
5407. 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.
541
5428. When **Back Page** is clicked, **PageTwo** is displayed, and only the @Watch decorated **info** method of the **NavigationContentMsgStack** child component in **pageTwoStack** is called.
543
5449. 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.
545
54610. When **Back Page** is clicked again, the initial page is displayed, and no method is called.
547
548![navigation-freeze.gif](figures/navigation-freeze.gif)
549
550### Reusing Components
551
552<!--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.
553
554#### Mixed Use of Component Reuse, if, and Component Freezing
555The 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.
556The procedure is as follows:
5571. Click **change flag** and change the value of **flag** to **false**.
558    -  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.
559    - **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.
5602. Click **change desc** to trigger the change of the member variable **desc** of **Page**.
561    - The change of \@State decorated **desc** will be notified to \@Link decorated **desc** of **ChildComponent**.
562    - 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.
5633. Click **change flag** again and change the value of **flag** to **true**.
564    - **ChildComponent** is attached to the component tree from the reuse pool.
565    - 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**.
566    - 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})**.
567
568
569```ts
570@Reusable
571@Component({freezeWhenInactive: true})
572struct ChildComponent {
573  @Link @Watch('descChange') desc: string;
574  @State count: number = 0;
575  descChange() {
576    console.info(`ChildComponent messageChange ${this.desc}`);
577  }
578
579  aboutToReuse(params: Record<string, ESObject>): void {
580    this.count = params.count as number;
581  }
582
583  aboutToRecycle(): void {
584    console.info(`ChildComponent has been recycled`);
585  }
586  build() {
587    Column() {
588      Text(`ChildComponent desc: ${this.desc}`)
589        .fontSize(20)
590      Text(`ChildComponent count ${this.count}`)
591        .fontSize(20)
592    }.border({width: 2, color: Color.Pink})
593  }
594}
595
596@Entry
597@Component
598struct Page {
599  @State desc: string = 'Hello World';
600  @State flag: boolean = true;
601  @State count: number = 0;
602  build() {
603    Column() {
604      Button(`change desc`).onClick(() => {
605        this.desc += '!';
606      })
607      Button(`change flag`).onClick(() => {
608        this.count++;
609        this.flag =! this.flag;
610      })
611      if (this.flag) {
612        ChildComponent({desc: this.desc, count: this.count})
613      }
614    }
615    .height('100%')
616  }
617}
618```
619#### Mixed Use of LazyForEach, Component Reuse, and Component Freezing
620In 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.
621However, 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**.
622In 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.
623
624Example:
6251. Swipe the list to the position whose index is 14. There are 15 **ChildComponent** in the visible area on the current page.
6262. During swiping:
627    - **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.
628    - 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.
6293. Click **change desc** to trigger the change of the member variable **desc** of **Page**.
630    - The change of \@State decorated **desc** will be notified to \@Link decorated **desc** of **ChildComponent**.
631    - **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.
632
633
634For details, see the following.
635![freeze](./figures/freezeResuable.png)
636You can listen for the changes by \@Trace, only 15 **ChildComponent** nodes are re-rendered.
637![freeze](./figures/traceWithFreeze.png)
638A complete sample code is as follows:
639```ts
640import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
641// Basic implementation of IDataSource used to listening for data.
642class BasicDataSource implements IDataSource {
643  private listeners: DataChangeListener[] = [];
644  private originDataArray: string[] = [];
645
646  public totalCount(): number {
647    return 0;
648  }
649
650  public getData(index: number): string {
651    return this.originDataArray[index];
652  }
653
654  // This method is called by the framework to add a listener to the LazyForEach data source.
655  registerDataChangeListener(listener: DataChangeListener): void {
656    if (this.listeners.indexOf(listener) < 0) {
657      console.info('add listener');
658      this.listeners.push(listener);
659    }
660  }
661
662  // This method is called by the framework to remove the listener from the LazyForEach data source.
663  unregisterDataChangeListener(listener: DataChangeListener): void {
664    const pos = this.listeners.indexOf(listener);
665    if (pos >= 0) {
666      console.info('remove listener');
667      this.listeners.splice(pos, 1);
668    }
669  }
670
671  // Notify LazyForEach that all child components need to be reloaded.
672  notifyDataReload(): void {
673    this.listeners.forEach(listener => {
674      listener.onDataReloaded();
675    })
676  }
677
678  // Notify LazyForEach that a child component needs to be added for the data item with the specified index.
679  notifyDataAdd(index: number): void {
680    this.listeners.forEach(listener => {
681      listener.onDataAdd(index);
682    })
683  }
684
685  // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt.
686  notifyDataChange(index: number): void {
687    this.listeners.forEach(listener => {
688      listener.onDataChange(index);
689    })
690  }
691
692  // Notify LazyForEach that the child component that matches the specified index needs to be deleted.
693  notifyDataDelete(index: number): void {
694    this.listeners.forEach(listener => {
695      listener.onDataDelete(index);
696    })
697  }
698
699  // Notify LazyForEach that data needs to be swapped between the from and to positions.
700  notifyDataMove(from: number, to: number): void {
701    this.listeners.forEach(listener => {
702      listener.onDataMove(from, to);
703    })
704  }
705}
706
707class MyDataSource extends BasicDataSource {
708  private dataArray: string[] = [];
709
710  public totalCount(): number {
711    return this.dataArray.length;
712  }
713
714  public getData(index: number): string {
715    return this.dataArray[index];
716  }
717
718  public addData(index: number, data: string): void {
719    this.dataArray.splice(index, 0, data);
720    this.notifyDataAdd(index);
721  }
722
723  public pushData(data: string): void {
724    this.dataArray.push(data);
725    this.notifyDataAdd(this.dataArray.length - 1);
726  }
727}
728
729@Reusable
730@Component({freezeWhenInactive: true})
731struct ChildComponent {
732  @Link @Watch('descChange') desc: string;
733  @State item: string = '';
734  @State index: number = 0;
735  descChange() {
736    console.info(`ChildComponent messageChange ${this.desc}`);
737  }
738
739  aboutToReuse(params: Record<string, ESObject>): void {
740    this.item = params.item;
741    this.index = params.index;
742  }
743
744  aboutToRecycle(): void {
745    console.info(`ChildComponent has been recycled`);
746  }
747  build() {
748    Column() {
749      Text(`ChildComponent index: ${this.index} item: ${this.item}`)
750        .fontSize(20)
751      Text(`desc: ${this.desc}`)
752        .fontSize(20)
753    }.border({width: 2, color: Color.Pink})
754  }
755}
756
757@Entry
758@Component
759struct Page {
760  @State desc: string = 'Hello World';
761  private data: MyDataSource = new MyDataSource();
762
763  aboutToAppear() {
764    for (let i = 0; i < 50; i++) {
765      this.data.pushData(`Hello ${i}`);
766    }
767  }
768
769  build() {
770    Column() {
771      Button(`change desc`).onClick(() => {
772        hiTraceMeter.startTrace('change decs', 1);
773        this.desc += '!';
774        hiTraceMeter.finishTrace('change decs', 1);
775      })
776      List({ space: 3 }) {
777        LazyForEach(this.data, (item: string, index: number) => {
778          ListItem() {
779            ChildComponent({index: index, item: item, desc: this.desc}).reuseId(index % 10 < 5 ? "1": "0")
780          }
781        }, (item: string) => item)
782      }.cachedCount(5)
783    }
784    .height('100%')
785  }
786}
787```
788#### Mixed Use of LazyForEach, if, Component Reuse, and Component Freezing
789
790Under the same parent custom component, reusable nodes may enter the reuse pool in different ways. For example:
791- Detaching from the cache area of LazyForEach by swiping.
792- Notifying the subnodes to detach by switching the if condition.
793
794In the following example:
7951. 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**.
7962. 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.
7973. In this case, the nodes detached through **LazyForEach** or **if** all enter the reuse pool under the **Page** node.
7984. Click **change desc** to update only the nine **ChildComponent** nodes on the page. For details, see figures below.
7995. 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.
8006. Click **change desc** again. The nodes attached through **if** and **LazyForEach** from the reuse pool can be re-rendered.
801
802Trace for component freezing enabled
803
804![traceWithFreezeLazyForeachAndIf](./figures/traceWithFreezeLazyForeachAndIf.png)
805
806Trace for component freezing disabled
807
808![traceWithFreezeLazyForeachAndIf](./figures/traceWithLazyForeachAndIf.png)
809
810
811A complete example is as follows:
812```
813import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
814class BasicDataSource implements IDataSource {
815  private listeners: DataChangeListener[] = [];
816  private originDataArray: string[] = [];
817
818  public totalCount(): number {
819    return 0;
820  }
821
822  public getData(index: number): string {
823    return this.originDataArray[index];
824  }
825
826  // This method is called by the framework to add a listener to the LazyForEach data source.
827  registerDataChangeListener(listener: DataChangeListener): void {
828    if (this.listeners.indexOf(listener) < 0) {
829      console.info('add listener');
830      this.listeners.push(listener);
831    }
832  }
833
834  // This method is called by the framework to remove the listener from the LazyForEach data source.
835  unregisterDataChangeListener(listener: DataChangeListener): void {
836    const pos = this.listeners.indexOf(listener);
837    if (pos >= 0) {
838      console.info('remove listener');
839      this.listeners.splice(pos, 1);
840    }
841  }
842
843  // Notify LazyForEach that all child components need to be reloaded.
844  notifyDataReload(): void {
845    this.listeners.forEach(listener => {
846      listener.onDataReloaded();
847    })
848  }
849
850  // Notify LazyForEach that a child component needs to be added for the data item with the specified index.
851  notifyDataAdd(index: number): void {
852    this.listeners.forEach(listener => {
853      listener.onDataAdd(index);
854    })
855  }
856
857  // Notify LazyForEach that the data item with the specified index has changed and the child component needs to be rebuilt.
858  notifyDataChange(index: number): void {
859    this.listeners.forEach(listener => {
860      listener.onDataChange(index);
861    })
862  }
863
864  // Notify LazyForEach that the child component that matches the specified index needs to be deleted.
865  notifyDataDelete(index: number): void {
866    this.listeners.forEach(listener => {
867      listener.onDataDelete(index);
868    })
869  }
870
871  // Notify LazyForEach that data needs to be swapped between the from and to positions.
872  notifyDataMove(from: number, to: number): void {
873    this.listeners.forEach(listener => {
874      listener.onDataMove(from, to);
875    })
876  }
877}
878
879class MyDataSource extends BasicDataSource {
880  private dataArray: string[] = [];
881
882  public totalCount(): number {
883    return this.dataArray.length;
884  }
885
886  public getData(index: number): string {
887    return this.dataArray[index];
888  }
889
890  public addData(index: number, data: string): void {
891    this.dataArray.splice(index, 0, data);
892    this.notifyDataAdd(index);
893  }
894
895  public pushData(data: string): void {
896    this.dataArray.push(data);
897    this.notifyDataAdd(this.dataArray.length - 1);
898  }
899}
900
901@Reusable
902@Component({freezeWhenInactive: true})
903struct ChildComponent {
904  @Link @Watch('descChange') desc: string;
905  @State item: string = '';
906  @State index: number = 0;
907  descChange() {
908    console.info(`ChildComponent messageChange ${this.desc}`);
909  }
910
911  aboutToReuse(params: Record<string, ESObject>): void {
912    this.item = params.item;
913    this.index = params.index;
914  }
915
916  aboutToRecycle(): void {
917    console.info(`ChildComponent has been recycled`);
918  }
919  build() {
920    Column() {
921      Text(`ChildComponent index: ${this.index} item: ${this.item}`)
922        .fontSize(20)
923      Text(`desc: ${this.desc}`)
924        .fontSize(20)
925    }.border({width: 2, color: Color.Pink})
926  }
927}
928
929@Entry
930@Component
931struct Page {
932  @State desc: string = 'Hello World';
933  @State flag: boolean = true;
934  private data: MyDataSource = new MyDataSource();
935
936  aboutToAppear() {
937    for (let i = 0; i < 50; i++) {
938      this.data.pushData(`Hello ${i}`);
939    }
940  }
941
942  build() {
943    Column() {
944      Button(`change desc`).onClick(() => {
945        hiTraceMeter.startTrace('change decs', 1);
946        this.desc += '!';
947        hiTraceMeter.finishTrace('change decs', 1);
948      })
949
950      Button(`change flag`).onClick(() => {
951        hiTraceMeter.startTrace('change flag', 1);
952        this.flag = !this.flag;
953        hiTraceMeter.finishTrace('change flag', 1);
954      })
955
956      List({ space: 3 }) {
957        LazyForEach(this.data, (item: string, index: number) => {
958          ListItem() {
959            ChildComponent({index: index, item: item, desc: this.desc}).reuseId(index % 10 < 5 ? "1": "0")
960          }
961        }, (item: string) => item)
962      }
963      .cachedCount(5)
964      .height('60%')
965
966      if (this.flag) {
967        ChildComponent({index: -1, item: 'Hello', desc: this.desc}).reuseId( "1")
968      }
969    }
970    .height('100%')
971  }
972}
973```
974
975## Constraints
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