目录

Optimizing Performance of UI Components

The process from app startup to UI display includes framework initialization, page loading, and layout rendering. The following figure shows the main process for page loading and layout rendering.

Figure 1 Initial page loading process

After the app is started, the page loading and rendering performance are related to the number of nodes in the FrameNode tree and the properties of each node. Therefore, to shorten the page loading and layout rendering duration, you can consider the following optimization solutions when using UI components at the frontend:

Avoiding Time-Consuming Operations During the Lifecycle of Custom Components

Figure 2 Process for the lifecycle of a custom component

As shown in the preceding figure, execute the lifecycle callback function aboutToAppear() after the custom component is created and before executing the build function. Otherwise, time-consuming operations will block the UI rendering, increasing the load of the UI main thread. Therefore, do not perform time-consuming operations during the lifecycle of a custom component. In time-consuming complex computing scenarios, the computing result can be cached. For time-consuming tasks that do not need to wait for results, you can use multiple threads to process the tasks and prevent the main thread from being blocked. It is recommended that only the initialization logic of the current component be performed in the aboutToAppear() lifecycle function. Other service logic can be processed in advance or delayed as required. Assume that a complex player object needs to be created in the child component of the video list on the home page. The creation of the object is time-consuming. If the object is created in the aboutToAppear() function of the component, when the home page is loaded and rendered, the rendering of each child component in the list waits until the corresponding player object is initialized and created. In this case, the page loading is time-consuming and even a white screen may occur. The pseudocode is as follows:

Negative example:

@Component
export struct VideoCard {
  // ...
  aboutToAppear(): void {
    // Create a complex object task. If the task takes 1s to execute, the component will be rendered again after 1s.
    this.createComplexVideoPlayer();
  }
  // ...
}

@Component
export struct CardList {
  @State videoList: VideoItem[] = getVideoList();

  build() {
    List() {
      ForEach(this.videoList, (item: VideoItem) => {
        ListItem() {
          VideoCard({ item })
        }
      }, (item: VideoItem) => item.id)
    }
  }
}

In this scenario, you can delay the time for creating a player object task. For example, you can calculate the position of the current component on the page. When the child component slides to one third of the page, a player object is created and the video is played. In this case, the main thread will not be blocked when the page is rendered for the first time. The sample code is as follows:

Positive example:

@Component
export struct VideoCard {
  @State isVideoInit: boolean = false;
  // ...
  build() {
    Column() {
      // Video Playback Component
    }
    .onAreaChange((old, newValue) => {
      if (!this.isVideoInit) {
        let positionY: number = newValue.position.y as number
        if (positionY < screenHeight / 3) {
          this.createComplexVideoPlayer();
          this.isVideoInit = true;
        }
      }
    })
  }
  // ...
}

@Component
export struct CardList {
  @State videoList: VideoItem[] = getVideoList();

  build() {
    List() {
      ForEach(this.videoList, (item: VideoItem) => {
        ListItem() {
          VideoCard({ item })
        }
      }, (item: VideoItem) => item.id)
    }
  }
}

For example, in the lifecycle aboutToAppear, do not use resource data as the argument of the getXXXSync API of @ohos.resourceManager (Resource Management). You are advised to use the resource ID as the argument. The recommended method is resourceManager.getStringSync($r('app.string.test').id). The following uses getStringSync as an example to test whether the two types of parameters differ in time consumption during usage.

Negative example:

import { hilog, hiTraceMeter } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct Index {
  @State message: string = 'getStringSync';
  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;

  aboutToAppear(): void {
    hiTraceMeter.startTrace('getStringSync', 1);
    // The input parameter of the getStringSync interface uses the resource directly, without using the resource ID.
    try {
      this.context.resourceManager.getStringSync($r('app.string.app_name'));
    } catch (error) {
      let err = error as BusinessError;
      hilog.warn(0x000, 'testTag', `getStringSync failed, code=${err.code}, message=${err.message}`);
    }
    hiTraceMeter.finishTrace('getStringSync', 1);
  }

  build() {
    RelativeContainer() {
      Text(this.message)
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
    }
    .height('100%')
    .width('100%')
  }
}

You can use Cold Start Analysis: Launch to capture performance trace points and check the time consumed for the analysis based on hiTraceMeter. The time consumed is 1.956 ms in the following case.

Positive example:

import { hilog, hiTraceMeter } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct Index {
  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
  @State message: string = 'getStringSyncAfter';

  aboutToAppear(): void {
    hiTraceMeter.startTrace('getStringSyncAfter', 2);
    // The input parameter of the getStringSync interface uses the resource ID.
    try {
      this.context.resourceManager.getStringSync($r('app.string.app_name').id);
    } catch (error) {
      let err = error as BusinessError;
      hilog.warn(0x000, 'testTag', `getStringSync failed, code=${err.code}, message=${err.message}`);
    }
    hiTraceMeter.finishTrace('getStringSyncAfter', 2);
  }

  build() {
    RelativeContainer() {
      Text(this.message)
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
    }
    .height('100%')
    .width('100%')
  }
}

You can use Cold Start Analysis: Launch to capture performance trace points and check the time consumed for the analysis based on hiTraceMeter. The time consumed is 0.071 ms.

Table 1 Time consumption statistics

Syntax

Duration

Resource information: getStringSync($r('app.string.app_name'))

1.956 ms

Resource ID: getStringSync($r('app.string.app_name').id)

0.071 ms

It can be concluded that it takes more time when resource information than resource ID is used as the parameter. Therefore, when a similar method is required, it is better to use the resource ID as a parameter to effectively reduce the time consumed for customizing the component lifecycle.

Registering Component Properties on Demand

When using components to develop app UIs, you need to set properties for each component and perform logic processing such as UI style and behavior processing. When a large number of properties are set for a single component and the component is widely used in the app, the settings greatly affect the app performance. For example, in the React Native (RN) framework development, 21 properties need to be set for a single component, and the component is used in the ForEach loop. In this scenario, the properties required by the app are unknown. Therefore, all properties are set on the component through property methods. However, in actual use, most apps use only a few of the properties, and other properties retain default values, which results in redundant settings of a large quantity of properties. The sample code snippet is as follows:

build() {
  Stack() {
    this.renderChildren()
  }
  .width(this.descriptor.layoutMetrics.frame.size.width)
  .height(this.descriptor.layoutMetrics.frame.size.height)
  .backgroundColor(convertColorSegmentsToString(this.descriptor.props.backgroundColor))
  .position({ y: this.descriptor.layoutMetrics.frame.origin.y, x: this.descriptor.layoutMetrics.frame.origin.x })
  .borderWidth(this.descriptor.props.borderWidth)
  .borderColor({
    left: convertColorSegmentsToString(this.descriptor.props.borderColor.left),
    top: convertColorSegmentsToString(this.descriptor.props.borderColor.top),
    right: convertColorSegmentsToString(this.descriptor.props.borderColor.right),
    bottom: convertColorSegmentsToString(this.descriptor.props.borderColor.bottom)
  })
  .borderRadius(this.descriptor.props.borderRadius)
  .borderStyle(this.getBorderStyle())
  .opacity(this.getOpacity())
  .transform(this.descriptor.props.transform != undefined ? convertMatrixArrayToMatrix4(this.descriptor.props.transform) : undefined)
  .clip(this.getClip())
  .hitTestBehavior(this.getHitTestMode())
  .shadow(this.getShadow())
}

In this scenario, during app development, the performance will be severely deteriorated when components with a large number of redundant properties need to be displayed in batches in the view. In this case, you can use Attribute Modifier to dynamically register component properties instead of using property methods to statically register component properties.

In comparison with using property methods to statically register component properties, using AttributeModifier to dynamically register component properties differs in the following aspects:

A simple public avatar component is used as an example to demonstrate the performance benefits of the AttributeModifier solution. The public avatar component requires that the user avatar be displayed on the UI if the user avatar data is available. For data without user avatars, the gray background and the first character of the user name are displayed. Currently, 1000 avatar components are displayed in the list, as shown in the following figure.

Figure 3 Avatar component page in a table

Set properties for the avatar component by referring to the following code:

import { util } from '@kit.ArkTS';

@Observed
class User {
  id: string;
  name: string;
  avatarImage: ResourceStr;

  constructor(id: string, name: string, avatarImage: ResourceStr) {
    this.id = id;
    this.name = name;
    this.avatarImage = avatarImage;
  }
}

//create data
const DEFAULT_BACKGROUND_COLOR = Color.Grey;
const getUsers = () => {
  return Array.from(Array(1000), (item: User, i: number) => {
    return new User(
      util.generateRandomUUID(),
      i % 2 === 0 ? 'Zhang San' : 'Li Si',
      i % 2 === 0 ? '' : $r('app.media.startIcon')
    );
  });
}

@Component
export struct AvatarGrid {
  @State users: User[] = getUsers();

  build() {
    Grid() {
      ForEach(this.users, (u: User) => {
        GridItem() {
          Avatar({ user: u })
        }
      }, (user: User) => user.id)
    }
    .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr')
    .columnsGap(4)
    .rowsGap(4)
  }
}

// Avatar component
@Component
struct Avatar {
  @ObjectLink user: User;

  build() {
    Row() {
      if (!this.user.avatarImage) {
        Text(this.user.name.charAt(0))
          .fontSize(28)
          .fontColor(Color.White)
          .fontWeight(FontWeight.Bold)
      }
    }
    .backgroundImage(this.user.avatarImage)
    .backgroundImageSize(ImageSize.Cover)
    .backgroundColor(DEFAULT_BACKGROUND_COLOR)
    .justifyContent(FlexAlign.Center)
    .size({ width: 50, height: 50 })
    .borderRadius(25)

    // .padding(2)
    // .margin(2)
    // .opacity(1)
    // .clip(false)
    // .layoutWeight(1)
    // .backgroundBlurStyle(BlurStyle.NONE)
    // .alignItems(VerticalAlign.Center)
    // .borderWidth(1)
    // .borderColor(Color.Pink)
    // .borderStyle(BorderStyle.Solid)
    // .expandSafeArea([SafeAreaType.SYSTEM])
    // .rotate({angle: 5})
    // .responseRegion({x: 0})
    // .mouseResponseRegion({x: 0})
    // .constraintSize({minWidth: 25})
    // .hitTestBehavior(HitTestMode.Default)
    // .backgroundImagePosition(Alignment.Center)
    // .foregroundBlurStyle(BlurStyle.NONE)
  }
}

To use AttributeModifier to dynamically register properties, add a custom class to implement the AttributeModifier API, and modify the property registration logic of the Avatar component. The modified code is as follows:

// 1.Custom Attribute Modifier, this class implements the AttributeModifier interface
class RowModifier implements AttributeModifier<RowAttribute> {
  private customImage: ResourceStr = '';
  private static instance: RowModifier;

  constructor() {
  }

  setCustomImage(customImage: ResourceStr) {
    this.customImage = customImage;
    return this;
  }

  // Adopting a singleton pattern avoids creating a new modifier for each component, increasing the performance overhead incurred by creating the
  public static getInstance(): RowModifier {
    if (!RowModifier.instance) {
      RowModifier.instance = new RowModifier();
    }
    return RowModifier.instance;
  }

  // 2.Implement the applyNormalAttribute method of the AttributeModifier interface to customize the logic of attribute setting.
  applyNormalAttribute(instance: RowAttribute) {
    if (this.customImage) {
      instance.backgroundImage(this.customImage);
      instance.backgroundImageSize(ImageSize.Cover);
    } else {
      instance.backgroundColor(Color.Blue);
      instance.justifyContent(FlexAlign.Center);
      // instance.padding(2)
      // instance.margin(2)
      // instance.opacity(1)
      // instance.clip(false)
      // instance.layoutWeight(1)
      // instance.backgroundBlurStyle(BlurStyle.NONE)
      // instance.alignItems(VerticalAlign.Center)
      // instance.borderWidth(1)
      // instance.borderColor(Color.Pink)
      // instance.borderStyle(BorderStyle.Solid)
      // instance.expandSafeArea([SafeAreaType.SYSTEM])
      // instance.rotate({ angle: 5 })
      // instance.responseRegion({x: 0})
      //instance.mouseResponseRegion({x: 0})
      // instance.constraintSize({minWidth: 25})
      // instance.hitTestBehavior(HitTestMode.Default)
      //instance.backgroundImagePosition(Alignment.Center)
      //instance.foregroundBlurStyle(BlurStyle.NONE)
    }
    instance.size({ width: 50, height: 50 });
    instance.borderRadius(25);
  }
}


@Component
struct Avatar {
  @ObjectLink user: User;

  build() {
    Row() {
      if (!this.user.avatarImage) {
        Text(this.user.name.charAt(0))
          .fontSize(28)
          .fontColor(Color.White)
          .fontWeight(FontWeight.Bold)
      }
    }
    // 3.Pass a custom RowModifier class as a parameter to enable on-demand property registration
    .attributeModifier(RowModifier.getInstance().setCustomImage(this.user.avatarImage))
  }
}

For the preceding two solutions, gradually increase the number of registered properties and use the launch analysis provided by DevEco Studio to obtain the page loading duration (value of PageRouterManager::LoadPage) and first frame duration (value of First Frame - App Phase) on the app side in the two solutions. The comparison is as follows:

Table 2 Time consumed by static and dynamic property registration when the number of properties is different
  

Static Property Registration

Dynamic Property Registration

Number of registered properties

6

12

18

24

6

12

18

24

PageRouterManager::LoadPage

639 ms 900 μs

668 ms 624 μs

719 ms 139 μs

764 ms 437 μs

640 ms 989 μs

662 ms 112 μs

705 ms 407 μs

717 ms 294 μs

First Frame - App Phase

45 ms 554 μs

45 ms 638 μs

52 ms 918 μs

52 ms 643 μs

44 ms 603 μs

43 ms 923 μs

46 ms 709 μs

46 ms 355 μs

Figure 4 Time consumed by LoadPage upon static and dynamic property registration under different numbers of properties
Figure 5 Time consumed by First Frame - App Phase upon static and dynamic property registration under different numbers of properties

According to the preceding data, benefits of dynamic registration are not significant if a small number of properties are to be registered. When the number of registered properties increases, the benefits of dynamic registration increase linearly.

Preferentially Using the @Builder Method Instead of Custom Components

When a custom component is used in ArkUI, a CustomNode node will be created in the backend FrameNode tree in the build phase, and a RenderNode node will be created in the rendering phase, as shown in the following figure.

Figure 6 Relationship between the frontend and backend UI component trees

Therefore, a smaller number of custom components used during app development, especially in loops, will reduce the number of CustomNode nodes in the FrameNode tree by multiple times, which effectively shortens the page loading and rendering duration. You can preferentially replace the custom component in an app with the @Builder function, which does not create a new tree node in the backend FrameNode tree.

The @Builder decorator strictly prohibits defining state variables or using lifecycle functions within it. Data interaction must be performed by passing parameters or accessing the state variables of the component to which the decorator belongs.

When a component is used only for display and does not require the internal state variables or lifecycle functions of the custom @Component, you can create an @Builder function instead of creating an @Component.

For example, when the ForEach loop is used to display the card list information, the page is displayed as follows:

Figure 7 Card list page

Use the custom component solution. The sample code is as follows:

import { util } from '@kit.ArkTS';

interface User {
  id: string;
  name: string;
  age?: number;
  avatarImage?: ResourceStr;
  //introduction: string;
  // ...
}

// create data
const DEFAULT_BACKGROUND_COLOR = Color.Pink;
const getUsers = () => {
  const USERS: User[] = [{
    id: '1',
    name: 'Zhang San',
  }, {
    id: '2',
    name: 'Li Si',
  }, {
    id: '3',
    name: 'Wang Wu',
  }];
  return Array.from(Array(30), (item: User, i: number) => {
    return {
      id: util.generateRandomUUID(),
      name: USERS[i%3].name,
      avatarImage: $r('app.media.startIcon'),
      age: 18 + i
    } as User;
  });
}

// User Card List Component
@Component
export struct UserCardList {
  @State users: User[] = getUsers();

  build() {
    List({ space: 8 }) {
      ForEach(this.users, (item: User) => {
        ListItem() {
          UserCard({ name: item.name, age: item.age, avatarImage: item.avatarImage })
        }
      }, (item: User) => item.id)
    }
    .alignListItem(ListItemAlign.Center)
  }
}

// User Card Customization Component
@Component
struct UserCard {
  @Prop avatarImage: ResourceStr;
  @Prop name: string;
  @Prop age: number;

  build() {
    Row() {
      Row() {
        Image(this.avatarImage)
          .size({ width: 50, height: 50 })
          .borderRadius(25)
          .margin(8)
        Text(this.name)
          .fontSize(30)
      }
      Text(`age:${this.age.toString()}`)
        .fontSize(20)
    }
    .backgroundColor(DEFAULT_BACKGROUND_COLOR)
    .justifyContent(FlexAlign.SpaceBetween)
    .borderRadius(8)
    .padding(8)
    .height(66)
    .width('80%')
  }
}

Replace the custom component UserCard with the @Builder function. The modified code is as follows:

// 1. Customizing @Builder Function Components
@Builder
function UserCardBuilder(name: string, age?: number, avatarImage?: ResourceStr) {
  Row() {
    Row() {
      Image(avatarImage)
        .size({ width: 50, height: 50 })
        .borderRadius(25)
        .margin(8)
      Text(name)
        .fontSize(30)
    }
    Text(`age:${age?.toString()}`)
      .fontSize(20)
  }
  .backgroundColor(Color.Blue)
  .justifyContent(FlexAlign.SpaceBetween)
  .borderRadius(8)
  .padding(8)
  .height(66)
  .width('80%')
}

@Component
export struct UserCardList {
  @State users: User[] = getUsers();

  build() {
    List({ space: 8 }) {
      ForEach(this.users, (item: User) => {
        ListItem() {
          // 2. Using the @Builder function in a build function
          UserCardBuilder(item.name, item.age, item.avatarImage)
        }
      }, (item: User) => item.id)
    }
    .alignListItem(ListItemAlign.Center)
  }
}

Increase the number of components from 30 to 3000. Use the profiler to obtain the time consumed by the page loading tag PageRouterManager::LoadPage and the page UI refresh task tag UITaskScheduler::FlushTask. The time consumed by the two solutions is compared as follows:

Figure 8 Comparison between the time consumed by the LoadPage tag in the two solutions
Figure 9 Comparison between the time consumed by the UITaskSchedule tag in the two solutions

As shown in the preceding figure, the @Builder solution outperforms the custom component solution in terms of page loading and UI page refreshing (including layout, rendering, and animation). As the number of components increases, the benefits increase linearly.

Using Layout Container Components Properly

If a large number of components need to be displayed, a layout container component is usually used to quickly implement the page layout. When the layout container component is used, multiple components need to be displayed at a time. As a result, the first frame may take a long time or even frame loss may occur. In this case, you can perform on-demand loading or lazy loading on child components in the container component. You can also use the capability for reusing components of the same structure. For details about how to optimize the performance of each layout container component, see Layout Optimization.

Sample Code