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 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 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 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 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 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