1# Precisely Controlling Render Scope
2
3In development of complex pages, precisely controlling the component render scope is especially important to speed up applications.
4
5This document exemplifies why and how the component render scope may be precisely controlled. For starters, you need to understand the re-render mechanism with state management.
6
7```ts
8@Observed
9class ClassA {
10  prop1: number = 0;
11  prop2: string = "This is Prop2";
12}
13@Component
14struct CompA {
15  @ObjectLink a: ClassA;
16  private sizeFont: number = 30; // the private variable does not invoke rendering
17  private isRenderText() : number {
18    this.sizeFont++; // the change of sizeFont will not invoke rendering, but showing that the function is called
19    console.log("Text prop2 is rendered");
20    return this.sizeFont;
21  }
22  build() {
23    Column() {
24      Text(this.a.prop2) // when this.a.prop2 changes, it will invoke Text rerendering
25        .fontSize(this.isRenderText()) // If the <Text> renders, the function isRenderText will be called.
26    }
27  }
28}
29@Entry
30@Component
31struct Page {
32  @State a: ClassA = new ClassA();
33  build() {
34    Row() {
35      Column() {
36        Text("Prop1: " + this.a.prop1)
37          .fontSize(50)
38          .margin({ bottom: 20 })
39        CompA({a: this.a})
40        Button("Change prop1")
41          .width(200)
42          .margin({ top: 20 })
43          .onClick(() => {
44            this.a.prop1 = this.a.prop1 + 1 ;
45          })
46      }
47      .width('100%')
48    }
49    .width('100%')
50    .height('100%')
51  }
52}
53```
54
55In the preceding example, when the value of **prop1** changes at the click of the button, although the components in **CompA** do not use **prop1**, you can still observe that the **Text** component associated with **prop2** is re-rendered – reflected by the component's enlarged font size and the console log of "Text prop2 is rendered." This indicates that, when a property (**prop1** in this example) of an @Observed decorated class object is changed, all components associated with any property of this object are re-rendered at once, even though these components may not directly use the changed property (i.e., the **Text** component using **prop** in this example). In this case, invisible, redundant re-renders occur. When a large number of components are involved in redundant re-renders, the render performance is greatly affected.
56
57The following figure shows the code running.
58
59![precisely-control-render-scope-01.gif](figures/precisely-control-render-scope-01.gif)
60
61The following is a typical example of redundant re-renders.
62
63```ts
64@Observed
65class UIStyle {
66  translateX: number = 0;
67  translateY: number = 0;
68  scaleX: number = 0.3;
69  scaleY: number = 0.3;
70  width: number = 336;
71  height: number = 178;
72  posX: number = 10;
73  posY: number = 50;
74  alpha: number = 0.5;
75  borderRadius: number = 24;
76  imageWidth: number = 78;
77  imageHeight: number = 78;
78  translateImageX: number = 0;
79  translateImageY: number = 0;
80  fontSize: number = 20;
81}
82@Component
83struct SpecialImage {
84  @ObjectLink uiStyle: UIStyle;
85  private isRenderSpecialImage() : number { // Function to show whether the component is rendered.
86    console.log("SpecialImage is rendered");
87    return 1;
88  }
89  build() {
90    Image($r('app.media.icon'))
91      .width(this.uiStyle.imageWidth)
92      .height(this.uiStyle.imageHeight)
93      .margin({ top: 20 })
94      .translate({
95        x: this.uiStyle.translateImageX,
96        y: this.uiStyle.translateImageY
97      })
98      .opacity(this.isRenderSpecialImage()) // If the <Image> is rendered, it will call the function.
99  }
100}
101@Component
102struct CompA {
103  @ObjectLink uiStyle: UIStyle
104  // The following functions are used to show whether the component is called to be rendered.
105  private isRenderColumn() : number {
106    console.log("Column is rendered");
107    return 1;
108  }
109  private isRenderStack() : number {
110    console.log("Stack is rendered");
111    return 1;
112  }
113  private isRenderImage() : number {
114    console.log("Image is rendered");
115    return 1;
116  }
117  private isRenderText() : number {
118    console.log("Text is rendered");
119    return 1;
120  }
121  build() {
122    Column() {
123      // When you compile this code in API version 9, the IDE may tell you that
124      // "Assigning the '@ObjectLink' decorated attribute 'uiStyle' to the '@ObjectLink' decorated attribute 'uiStyle' is not allowed. <etsLint>"
125      // Yet, you can still run the code by Previewer.
126      SpecialImage({
127        uiStyle: this.uiStyle
128      })
129      Stack() {
130        Column() {
131            Image($r('app.media.icon'))
132              .opacity(this.uiStyle.alpha)
133              .scale({
134                x: this.uiStyle.scaleX,
135                y: this.uiStyle.scaleY
136              })
137              .padding(this.isRenderImage())
138              .width(300)
139              .height(300)
140        }
141        .width('100%')
142        .position({ y: -80 })
143        Stack() {
144          Text("Hello World")
145            .fontColor("#182431")
146            .fontWeight(FontWeight.Medium)
147            .fontSize(this.uiStyle.fontSize)
148            .opacity(this.isRenderText())
149            .margin({ top: 12 })
150        }
151        .opacity(this.isRenderStack())
152        .position({
153          x: this.uiStyle.posX,
154          y: this.uiStyle.posY
155        })
156        .width('100%')
157        .height('100%')
158      }
159      .margin({ top: 50 })
160      .borderRadius(this.uiStyle.borderRadius)
161      .opacity(this.isRenderStack())
162      .backgroundColor("#FFFFFF")
163      .width(this.uiStyle.width)
164      .height(this.uiStyle.height)
165      .translate({
166        x: this.uiStyle.translateX,
167        y: this.uiStyle.translateY
168      })
169      Column() {
170        Button("Move")
171          .width(312)
172          .fontSize(20)
173          .backgroundColor("#FF007DFF")
174          .margin({ bottom: 10 })
175          .onClick(() => {
176            animateTo({
177              duration: 500
178            },() => {
179              this.uiStyle.translateY = (this.uiStyle.translateY + 180) % 250;
180            })
181          })
182        Button("Scale")
183          .borderRadius(20)
184          .backgroundColor("#FF007DFF")
185          .fontSize(20)
186          .width(312)
187          .onClick(() => {
188            this.uiStyle.scaleX = (this.uiStyle.scaleX + 0.6) % 0.8;
189          })
190      }
191      .position({
192        y:666
193      })
194      .height('100%')
195      .width('100%')
196
197    }
198    .opacity(this.isRenderColumn())
199    .width('100%')
200    .height('100%')
201
202  }
203}
204@Entry
205@Component
206struct Page {
207  @State uiStyle: UIStyle = new UIStyle();
208  build() {
209    Stack() {
210      CompA({
211        uiStyle: this.uiStyle
212      })
213    }
214    .backgroundColor("#F1F3F5")
215  }
216}
217```
218
219In the above example, **uiStyle** defines multiple properties, which are each associated with multiple components. When some of these properties are changed at the click of a button, all the components associated with **uiStyle** are re-rendered according to the mechanism described above, even though they actually do not need to be re-rendered (because the properties of these components are not changed). The re-renders of these components can be observed through a series of defined **isRender** functions. When **Move** is clicked to perform the translation animation, the values of **translateX** and **translateY** change multiple times. As a result, redundant re-renders occur at each frame, which greatly worsen the application performance.
220
221The following figure shows the code running.
222
223![precisely-control-render-scope-02.gif](figures/precisely-control-render-scope-02.gif)
224
225To precisely control the component render scope and avoid redundant re-renders, it is recommended that you divide a large property object into several small property objects.
226
227To achieve this purpose, it is first necessary to understand the mechanism for property change observation.
228
229Below is sample code:
230
231```TS
232@Observed
233class ClassB {
234  subProp1: number = 100;
235}
236@Observed
237class ClassA {
238  prop1: number = 0;
239  prop2: string = "This is Prop2";
240  prop3: ClassB = new ClassB();
241}
242@Component
243struct CompA {
244  @ObjectLink a: ClassA;
245  private sizeFont: number = 30; // the private variable does not invoke rendering
246  private isRenderText() : number {
247    this.sizeFont++; // the change of sizeFont will not invoke rendering, but showing that the function is called
248    console.log("Text prop2 is rendered");
249    return this.sizeFont;
250  }
251  build() {
252    Column() {
253      Text(this.a.prop2) // When this.a.prop1 changes, it will invoke <Text> re-rendering.
254        .margin({ bottom: 10 })
255        .fontSize(this.isRenderText()) // If the <Text> renders, the function isRenderText will be called.
256      Text("subProp1 : " + this.a.prop3.subProp1) //the Text can not observe the change of subProp1
257        .fontSize(30)
258    }
259  }
260}
261@Entry
262@Component
263struct Page {
264  @State a: ClassA = new ClassA();
265  build() {
266    Row() {
267      Column() {
268        Text("Prop1: " + this.a.prop1)
269          .margin({ bottom: 20 })
270          .fontSize(50)
271        CompA({a: this.a})
272        Button("Change prop1")
273          .width(200)
274          .fontSize(20)
275          .backgroundColor("#FF007DFF")
276          .margin({
277            top: 10,
278            bottom: 10
279          })
280          .onClick(() => {
281            this.a.prop1 = this.a.prop1 + 1 ;
282          })
283        Button("Change subProp1")
284          .width(200)
285          .fontSize(20)
286          .backgroundColor("#FF007DFF")
287          .onClick(() => {
288            this.a.prop3.subProp1 = this.a.prop3.subProp1 + 1;
289          })
290      }
291      .width('100%')
292    }
293    .width('100%')
294    .height('100%')
295  }
296}
297```
298
299In the preceding example, when **Change subProp1** is clicked, you can find that the page is not re-rendered. This is because the change to **subProp1** is not observed by the component. When **Change prop1** is clicked, the page is re-rendered, with the latest values of **prop1** and **subProp1** displayed. According to the ArkUI state management mechanism, the state variable can only observe the change at the first layer. For **Change subProp1**, the property value changes at the second layer and therefore cannot be observed. In other words, the change of **this.a.prop3.subProp1** does not cause component re-renders, even if the value of **subProp1** has changed. In comparison, the change of **this.a.prop1** causes component re-renders.
300
301The following figure shows the code running.
302
303![precisely-control-render-scope-03.gif](figures/precisely-control-render-scope-03.gif)
304
305With this mechanism of property change observation, the render scope of components can be precisely controlled.
306
307```ts
308@Observed
309class ClassB {
310  subProp1: number = 100;
311}
312@Observed
313class ClassA {
314  prop1: number = 0;
315  prop2: string = "This is Prop2";
316  prop3: ClassB = new ClassB();
317}
318@Component
319struct CompA {
320  @ObjectLink a: ClassA;
321  @ObjectLink b: ClassB; // A new @ObjectLink decorated variable.
322  private sizeFont: number = 30;
323  private isRenderText() : number {
324    this.sizeFont++;
325    console.log("Text prop2 is rendered");
326    return this.sizeFont;
327  }
328  private isRenderTextSubProp1() : number {
329    this.sizeFont++;
330    console.log("Text subProp1 is rendered");
331    return this.sizeFont;
332  }
333  build() {
334    Column() {
335      Text(this.a.prop2) // When this.a.prop1 changes, it will invoke <Text> re-rendering.
336        .margin({ bottom: 10 })
337        .fontSize(this.isRenderText()) // If the <Text> renders, the function isRenderText will be called.
338      Text("subProp1 : " + this.b.subProp1) // Use directly b rather than a.prop3.
339        .fontSize(30)
340        .opacity(this.isRenderTextSubProp1())
341    }
342  }
343}
344@Entry
345@Component
346struct Page {
347  @State a: ClassA = new ClassA();
348  build() {
349    Row() {
350      Column() {
351        Text("Prop1: " + this.a.prop1)
352          .margin({ bottom: 20 })
353          .fontSize(50)
354        CompA({
355          a: this.a,
356          b: this.a.prop3
357        })
358        Button("Change prop1")
359          .width(200)
360          .fontSize(20)
361          .backgroundColor("#FF007DFF")
362          .margin({
363            top: 10,
364            bottom: 10
365          })
366          .onClick(() => {
367            this.a.prop1 = this.a.prop1 + 1 ;
368          })
369        Button("Change subProp1")
370          .width(200)
371          .fontSize(20)
372          .backgroundColor("#FF007DFF")
373          .margin({
374            top: 10,
375            bottom: 10
376          })
377          .onClick(() => {
378            this.a.prop3.subProp1 = this.a.prop3.subProp1 + 1;
379          })
380      }
381      .width('100%')
382    }
383    .width('100%')
384    .height('100%')
385  }
386}
387```
388
389In the preceding example, a new variable **b** decorated by @ObjectLink is defined in **CompA**. When **CompA** is created on the page, **prop3** in object **a** is passed to **b**. In this way, **b** can be directly used in **CompA**. This means that, in effect, **CompA** is associated with **b** and can observe the change of **subProp1** in **b**. When **Change subProp1** is clicked, the associated **Text** component is re-rendered, but other components are not (because these components are associated with **a**). Similarly, changes to other properties in **a** do not cause the **Text** component to be re-rendered.
390
391The following figure shows the code running.
392
393![precisely-control-render-scope-04.gif](figures/precisely-control-render-scope-04.gif)
394
395By using the aforementioned method, properties in the foregoing complex redundant re-render scenario can be divided to optimize performance.
396
397```ts
398@Observed
399class NeedRenderImage { // Properties only used in the same component can be divided into the same new divided class.
400  public translateImageX: number = 0;
401  public translateImageY: number = 0;
402  public imageWidth:number = 78;
403  public imageHeight:number = 78;
404}
405@Observed
406class NeedRenderScale { // Properties usually used together can be divided into the same new child class.
407  public scaleX: number = 0.3;
408  public scaleY: number = 0.3;
409}
410@Observed
411class NeedRenderAlpha { // Properties that may be used in different places can be divided into the same new child class.
412  public alpha: number = 0.5;
413}
414@Observed
415class NeedRenderSize { // Properties usually used together can be divided into the same new child class.
416  public width: number = 336;
417  public height: number = 178;
418}
419@Observed
420class NeedRenderPos { // Properties usually used together can be divided into the same new child class.
421  public posX: number = 10;
422  public posY: number = 50;
423}
424@Observed
425class NeedRenderBorderRadius { // Properties that may be used in different places can be divided into the same new child class.
426  public borderRadius: number = 24;
427}
428@Observed
429class NeedRenderFontSize { // Properties that may be used in different places can be divided into the same new child class.
430  public fontSize: number = 20;
431}
432@Observed
433class NeedRenderTranslate { // Properties usually used together can be divided into the same new child class.
434  public translateX: number = 0;
435  public translateY: number = 0;
436}
437@Observed
438class UIStyle {
439  // Define a new variable instead of using the old one.
440  needRenderTranslate: NeedRenderTranslate = new NeedRenderTranslate();
441  needRenderFontSize: NeedRenderFontSize = new NeedRenderFontSize();
442  needRenderBorderRadius: NeedRenderBorderRadius = new NeedRenderBorderRadius();
443  needRenderPos: NeedRenderPos = new NeedRenderPos();
444  needRenderSize: NeedRenderSize = new NeedRenderSize();
445  needRenderAlpha: NeedRenderAlpha = new NeedRenderAlpha();
446  needRenderScale: NeedRenderScale = new NeedRenderScale();
447  needRenderImage: NeedRenderImage = new NeedRenderImage();
448}
449@Component
450struct SpecialImage {
451  @ObjectLink uiStyle : UIStyle;
452  @ObjectLink needRenderImage: NeedRenderImage // Receive the new class from its parent component.
453  private isRenderSpecialImage() : number { // Function to show whether the component is rendered.
454    console.log("SpecialImage is rendered");
455    return 1;
456  }
457  build() {
458    Image($r('app.media.icon'))
459      .width(this.needRenderImage.imageWidth) // Attention: Use this.needRenderImage.xxx rather than this.uiStyle.needRenderImage.xxx.
460      .height(this.needRenderImage.imageHeight)
461      .margin({top:20})
462      .translate({
463        x: this.needRenderImage.translateImageX,
464        y: this.needRenderImage.translateImageY
465      })
466      .opacity(this.isRenderSpecialImage()) // If the <Image> is rendered, it will call the function.
467  }
468}
469@Component
470struct CompA {
471  @ObjectLink uiStyle: UIStyle;
472  @ObjectLink needRenderTranslate: NeedRenderTranslate; // Receive the new class from its parent component.
473  @ObjectLink needRenderFontSize: NeedRenderFontSize;
474  @ObjectLink needRenderBorderRadius: NeedRenderBorderRadius;
475  @ObjectLink needRenderPos: NeedRenderPos;
476  @ObjectLink needRenderSize: NeedRenderSize;
477  @ObjectLink needRenderAlpha: NeedRenderAlpha;
478  @ObjectLink needRenderScale: NeedRenderScale;
479  // The following functions are used to show whether the component is called to be rendered.
480  private isRenderColumn() : number {
481    console.log("Column is rendered");
482    return 1;
483  }
484  private isRenderStack() : number {
485    console.log("Stack is rendered");
486    return 1;
487  }
488  private isRenderImage() : number {
489    console.log("Image is rendered");
490    return 1;
491  }
492  private isRenderText() : number {
493    console.log("Text is rendered");
494    return 1;
495  }
496  build() {
497    Column() {
498      // When you compile this code in API version 9, the IDE may tell you that
499      // "Assigning the '@ObjectLink' decorated attribute 'uiStyle' to the '@ObjectLink' decorated attribute 'uiStyle' is not allowed. <etsLint>"
500      // "Assigning the '@ObjectLink' decorated attribute 'uiStyle' to the '@ObjectLink' decorated attribute 'needRenderImage' is not allowed. <etsLint>"
501      // Yet, you can still run the code by Previewer.
502      SpecialImage({
503        uiStyle: this.uiStyle,
504        needRenderImage: this.uiStyle.needRenderImage // Send it to its child.
505      })
506      Stack() {
507        Column() {
508          Image($r('app.media.icon'))
509            .opacity(this.needRenderAlpha.alpha)
510            .scale({
511              x: this.needRenderScale.scaleX, // Use this.needRenderXxx.xxx rather than this.uiStyle.needRenderXxx.xxx.
512              y: this.needRenderScale.scaleY
513            })
514            .padding(this.isRenderImage())
515            .width(300)
516            .height(300)
517        }
518        .width('100%')
519        .position({ y: -80 })
520
521        Stack() {
522          Text("Hello World")
523            .fontColor("#182431")
524            .fontWeight(FontWeight.Medium)
525            .fontSize(this.needRenderFontSize.fontSize)
526            .opacity(this.isRenderText())
527            .margin({ top: 12 })
528        }
529        .opacity(this.isRenderStack())
530        .position({
531          x: this.needRenderPos.posX,
532          y: this.needRenderPos.posY
533        })
534        .width('100%')
535        .height('100%')
536      }
537      .margin({ top: 50 })
538      .borderRadius(this.needRenderBorderRadius.borderRadius)
539      .opacity(this.isRenderStack())
540      .backgroundColor("#FFFFFF")
541      .width(this.needRenderSize.width)
542      .height(this.needRenderSize.height)
543      .translate({
544        x: this.needRenderTranslate.translateX,
545        y: this.needRenderTranslate.translateY
546      })
547
548      Column() {
549        Button("Move")
550          .width(312)
551          .fontSize(20)
552          .backgroundColor("#FF007DFF")
553          .margin({ bottom: 10 })
554          .onClick(() => {
555            animateTo({
556              duration: 500
557            }, () => {
558              this.needRenderTranslate.translateY = (this.needRenderTranslate.translateY + 180) % 250;
559            })
560          })
561        Button("Scale")
562          .borderRadius(20)
563          .backgroundColor("#FF007DFF")
564          .fontSize(20)
565          .width(312)
566          .margin({ bottom: 10 })
567          .onClick(() => {
568            this.needRenderScale.scaleX = (this.needRenderScale.scaleX + 0.6) % 0.8;
569          })
570        Button("Change Image")
571          .borderRadius(20)
572          .backgroundColor("#FF007DFF")
573          .fontSize(20)
574          .width(312)
575          .onClick(() => { // In the parent component, still use this.uiStyle.needRenderXxx.xxx to change the properties.
576            this.uiStyle.needRenderImage.imageWidth = (this.uiStyle.needRenderImage.imageWidth + 30) % 160;
577            this.uiStyle.needRenderImage.imageHeight = (this.uiStyle.needRenderImage.imageHeight + 30) % 160;
578          })
579      }
580      .position({
581        y: 616
582      })
583      .height('100%')
584      .width('100%')
585    }
586    .opacity(this.isRenderColumn())
587    .width('100%')
588    .height('100%')
589  }
590}
591@Entry
592@Component
593struct Page {
594  @State uiStyle: UIStyle = new UIStyle();
595  build() {
596    Stack() {
597      CompA({
598        uiStyle: this.uiStyle,
599        needRenderTranslate: this.uiStyle.needRenderTranslate, // Send all the new class child need.
600        needRenderFontSize: this.uiStyle.needRenderFontSize,
601        needRenderBorderRadius: this.uiStyle.needRenderBorderRadius,
602        needRenderPos: this.uiStyle.needRenderPos,
603        needRenderSize: this.uiStyle.needRenderSize,
604        needRenderAlpha: this.uiStyle.needRenderAlpha,
605        needRenderScale: this.uiStyle.needRenderScale
606      })
607    }
608    .backgroundColor("#F1F3F5")
609  }
610}
611```
612
613The following figure shows the code running.
614
615![precisely-control-render-scope-05.gif](figures/precisely-control-render-scope-05.gif)
616
617In the preceding example, the 15 properties in the original class are divided into eight child classes, and the corresponding adaptation is performed on the binding between properties and components. Division of properties complies with the following principles:
618
619- Properties that are only used in the same component can be divided into the same new child class, that is, **NeedRenderImage** in the example. This mode of division is applicable to the scenario where components are frequently re-rendered due to changes of unassociated properties. In this scenario, divide the properties or review the view model design.
620- Properties that are frequently used together can be divided into the same new child class, that is, **NeedRenderScale**, **NeedRenderTranslate**, **NeedRenderPos**, and **NeedRenderSize** in the example. This mode of division is applicable to the scenario where properties often appear in pairs or are applied to the same style, for example, **.translate**, **.position**, and **.scale** (which usually receive an object as a parameter).
621- Properties that may be used in different places should be divided into a new child class, that is, **NeedRenderAlpha**, **NeedRenderBorderRadius**, and **NeedRenderFontSize** in the example. This mode of division is applicable to the scenario where a property works on multiple components or is not related to other properties, for example, **.opacity** and **.borderRadius** (which usually work on their own).
622
623After properties are divided, use the following format to bind components using the properties:
624
625```ts
626.property(this.needRenderXxx.xxx)
627
628// sample
629Text("some text")
630.width(this.needRenderSize.width)
631.height(this.needRenderSize.height)
632.opacity(this.needRenderAlpha.alpha)
633```
634
635If changes of a property apply to the parent component, the property can be changed through the outer parent class.
636
637```ts
638// In parent Component
639this.parent.needRenderXxx.xxx = x;
640
641// Example
642this.uiStyle.needRenderImage.imageWidth = (this.uiStyle.needRenderImage.imageWidth + 20) % 60;
643```
644
645If changes of a property apply to the child component, it is recommended that the property be changed through the new child class.
646
647```ts
648// In child Component
649this.needRenderXxx.xxx = x;
650
651// Example
652this.needRenderScale.scaleX = (this.needRenderScale.scaleX + 0.6) % 1
653```
654
655When dividing properties to speed up applications, focus on properties that change frequently.
656
657If you want to use the divided properties in the parent component, you are advised to define a new @State decorated state variable and use them together.
658
659```ts
660@Observed
661class NeedRenderProperty {
662  public property: number = 1;
663};
664@Observed
665class SomeClass {
666  needRenderProperty: NeedRenderProperty = new NeedRenderProperty();
667}
668@Entry
669@Component
670struct Page {
671  @State someClass: SomeClass = new SomeClass();
672  @State needRenderProperty: NeedRenderProperty = this.someClass.needRenderProperty
673  build() {
674    Row() {
675      Column() {
676        Text("property value: " + this.needRenderProperty.property)
677          .fontSize(30)
678          .margin({ bottom: 20 })
679        Button("Change property")
680          .onClick(() => {
681            this.needRenderProperty.property++;
682          })
683      }
684      .width('100%')
685    }
686    .width('100%')
687    .height('100%')
688  }
689}
690```
691