目录

Development Practices of Custom Component Reuse

Overview

Component reuse refers to a mechanism where custom components are cached in a pool after being removed from the component tree. When new instances of the same component type are needed, the system reuses cached component objects instead of creating new ones.

In app development, component reuse is a critical strategy for enhancing UI performance and ensuring a smooth user experience. Proper use of reusable components provides two key advantages: (1) It avoids the frequent creation and destruction of objects, reducing garbage collection frequency. (2) It enables data binding with cached components for display, eliminating the need to create new views and thereby lowering computational costs and improving rendering efficiency.

A common scenario for component reuse is displaying large datasets in scrollable lists. During rapid user scrolling, traditional approaches repeatedly create and destroy list items, potentially causing performance issues like lag. With component reuse, previously created list item views are recycled, significantly improving scroll smoothness.

This topic introduces several component reuse development scenarios to help you better understand the reuse mechanism and effectively apply it to optimize your app's performance.

While this topic primarily illustrates component reuse through list scenarios, the technique applies to any situation involving component destruction and re-creation. To illustrate, the following situations are included:

  1. Frequent creation and destruction of child components in scrolling scenarios, such as within layout containers like List, Grid, WaterFlow, and Swiper.
  2. Repeated switching of conditionally rendered control branches in the UI, especially when the component tree structure within those branches is relatively complex.

Component Reuse Within a Single List

[h2]Description

Component reuse within scrollable lists represents a fundamental optimization pattern in app development. As users scroll through a list, list items exiting the visible area (plus the buffer zone) are detached from the component tree and placed in a cache pool; when new list items come into view, objects are retrieved from the cache pool, data is bound to them, and they are rendered on the screen.

In real-world scenarios, a single list may present one or more types of list item structures. These can be further divided into the following sub-scenarios:

[h2]Implementation

ArkUI provides the @Reusable to implement the reuse of custom components. The following figure shows the implementation principle.

  1. A custom component (for example, listItem) decorated with @Reusable is removed from the component tree once it scrolls out of the visible area. The component's object instance is then placed into a CustomNode virtual node (which corresponds one-to-one with the custom component).
  2. During continuous scrolling, the RecycleManager of the list reclaims these CustomNode virtual nodes, groups them based on the reuse ID, and forms a CachedRecycleNodes set, that is, the reuse cache pool of view objects.
  3. When the list continues to scroll and a new listItem needs to be displayed, the RecycleManager first searches the reuse cache pool for the view object corresponding to the reuseId. It then binds the new data to that view, reuses the node, and adds it back to the component tree.

[h2]How to Develop

  1. Define reusable components: Use the @Reusable decorator to decorate custom components that should be reusable.

  2. Implement the reuse callback: Implement the aboutToReuse() lifecycle callback for reusable components. When the component is re-added to the component tree from the cache pool, the aboutToReuse() lifecycle callback is triggered. The component's constructor parameters are passed into this callback, where you can handle data update as needed.

  3. Use reusable components in the layout: Set reuseId to divide the components into different reuse groups to distinguish them in the cache pool. If no reuseId is set, the component's name is used by default as the reuseId.

    @Entry
    @Component
    struct Index {
      // ...
    
      build() {
        Column() {
          // ...
          if (this.switch) {
            ReusableComponent({ text: this.typeStr })
              .reuseId(this.typeStr)
          }
        }
        // ...
      }
    
      // ...
    }
    
    @Reusable
    @Component
    struct ReusableComponent {
      @State text: string = ''
    
      aboutToReuse(params: Record<string, Object>): void {
        this.text = params.text as string;
      }
    
      build() {
        // ...
      }
    }

Note:

[h2]List Items with the Same Structure

For lists where all items share identical element types and layout structures, the list item components can be treated as the basic units for reuse logic.

Implementation steps:

  1. Encapsulate the list item as a custom component named ItemView and decorated with the @Reusable decorator.
  2. In the aboutToReuse() method of the ItemView component, implement the logic for binding new data.
  3. Use the ItemView component inside a LazyForEach() loop, and set its reuseId to enable reuse behavior.

In the following example, the @Reusable decorator and the aboutToReuse() method are added to the ItemView component and used in OneTypeItemPage, and reuseId('item_id') is set for the ItemView component, indicating that item_id is used as the reuse group ID.

@Component
export struct OneTypeItemPage {
  // ...
  build() {
    NavDestination() {
      Column() {
        List() {
          LazyForEach(this.dataSource, (item: ItemData) => {
            ItemView({ title: item.title, from: item.from, tail: item.tail })
              .reuseId('item_id')
          }, (item: ItemData) => item.id.toString())
        }
        // ...
      }
      // ...
    }
    // ...
  }
}

@Reusable
@Component
struct ItemView {
  @State title: string | Resource = '';
  @State from: string | Resource = '';
  @State tail: string | Resource = '';

  aboutToReuse(params: Record<string, Object>): void {
    this.title = params.title as string;
    this.from = params.from as string;
    this.tail = params.tail as string;
  }

  build() {
    // ...
  }
}

[h2]List Items with Different Structures

When a list contains multiple types of list items—such as text-only list item, list item with a single image, and list item with multiple images, as shown below—each type of list item can be treated as a separate unit for reuse.

During scrolling, different types of list items are recycled into their respective cache pools. When reuse is required, the system will search for the appropriate view cache based on the item type and display the corresponding view.

Implementation steps:

  1. Encapsulate each type of list item into separate custom components and decorate each with the @Reusable decorator.
  2. In the aboutToReuse() method of each component, implement the logic for binding new data.
  3. In the LazyForEach() loop, use the if logic to select and lay out the appropriate list item components. Set a unique reuseId for each type of list item to distinguish their reuse groups.
@Component
export struct MultiTypeItemPage {
  // ...

  build() {
    NavDestination() {
      Column() {
        List() {
          LazyForEach(this.dataSource, (item: ItemData) => {
            if (item.type === 0) {
              TextTypeItemView({ item: item })
                .reuseId('text_item_id')
            } else if (item.type === 1) {
              ImageTypeItemView({ item: item })
                .reuseId('image_item_id')
            } else if (item.type === 2) {
              ThreeImageTypeItemView({ item: item })
                .reuseId('three_image_item_id')
            }
          }, (item: ItemData) => item.id.toString())
        }
        // ...
      }
      // ...
    }
    // ...
  }
}

@Reusable
@Component
struct TextTypeItemView {
  // ...
}


@Reusable
@Component
struct ImageTypeItemView {
  // ...
}

@Reusable
@Component
struct ThreeImageTypeItemView {
  // ...
}

[h2]List Items with Composable Child Components

For list items exhibiting partial structural similarities, for example, shared header and footer with variable content regions, a compositional reuse strategy applies. In the list example shown below, child components within each list item are arranged vertically. They share a common top text title and bottom timestamp, but differ in the middle section, which can contain a single image, multiple images, or video content.

Therefore, you can create five reusable child components and, by combining these child components, construct different types of list items.

Implementation steps:

  1. Create separate custom components for each child component type: single image, multiple images, video, top title, bottom timestamp. Decorate each of these child components with the @Reusable decorator.
  2. In the aboutToReuse() method of each component, implement the logic for binding new data.
  3. Use @Builder to create three different builder functions. These functions correspond to three types of list items: list item with a single image, list item with multiple images, list item with a video.
  4. In LazyForEach(), conditionally invoke the appropriate @Builder function based on the service logic.

The reason for using @Builder instead of directly nesting subcomponents is as follows:

Because the reuse cache pool is maintained at the level of the custom component itself, nesting child components within it can lead to fragmentation of the cache pool, causing reuse to fail. In contrast, using @Builder allows internal custom components to remain grouped within the same cache pool, thereby enabling effective and mutual reuse.

Positive example:

The itemBuilderSingleImage() function is implemented using the @Builder decorator. When this.itemBuilderSingleImage(item) is called in ComposableItemPage, the custom components in itemBuilderSingleImage() are grouped in the cache pool corresponding to ComposableItemPage. That is, the reusable components such as TopView, MiddleSingleImageView, and BottomView are grouped in the same cache pool. When other @Builder-decorated functions are called in ComposableItemPage, the components decorated with @Reusable can also be reused.

@Component
export struct ComposableItemPage {
  // ...

  @Builder
  itemBuilderSingleImage(item: ItemData) {
    TopView({ item: item }).reuseId('top_id')
    MiddleSingleImageView({ item: item }).reuseId('middle_image_id')
    BottomView({ item: item }).reuseId('bottom_id')
  }

  @Builder
  itemBuilderThreeImage(item: ItemData) {
    TopView({ item: item }).reuseId('top_id')
    MiddleThreeImageView({ item: item }).reuseId('middle_three_image_id')
    BottomView({ item: item }).reuseId('bottom_id')
  }

  @Builder
  itemBuilderVideoImage(item: ItemData) {
    TopView({ item: item }).reuseId('top_id')
    MiddleVideoView({ item: item }).reuseId('middle_video_id')
    BottomView({ item: item }).reuseId('bottom_id')
  }

  build() {
    NavDestination() {
      Column() {
        List() {
          LazyForEach(this.dataSource, (item: ItemData) => {
            ListItem() {
              Column() {
                if (item.type === 0) {
                  this.itemBuilderSingleImage(item)
                } else if (item.type === 1) {
                  this.itemBuilderThreeImage(item)
                } else if (item.type === 2) {
                  this.itemBuilderVideoImage(item)
                }
              }
              // ...
            }
          }, (item: ItemData) => item.id.toString())
        }
        // ...
      }
      // ...
    }
    // ...
  }
}

@Reusable
@Component
struct TopView {
  // ...
}

@Reusable
@Component
struct BottomView {
  // ...
}

@Reusable
@Component
struct MiddleSingleImageView {
  // ...
}

@Reusable
@Component
struct MiddleThreeImageView {
  // ...
}

@Reusable
@Component
struct MiddleVideoView {
  // ...
}

Negative example:

If you change the @Builder-decorated functions such as itemBuilderSingleImage() and itemBuilderThreeImage() to the @Component-decorated functions, for example, SingleImageComponent and ThreeImageComponent, the components in SingleImageComponent, such as TopView, MiddleSingleImageView, and BottomView will be grouped only in the cache pool corresponding to SingleImageComponent. The same rule applies to ThreeImageComponent. These @Reusable-decorated components are in different @Component-decorated cache pools. Therefore, the reuse does not take effect when ComposableItemPage is drawn in the last layout.

@Component
export struct ComposableItemPage {
  // ...
  build() {
    NavDestination() {
      Column() {
        List() {
          LazyForEach(this.dataSource, (item: ItemData) => {
            ListItem() {
              Column() {
                if (item.type === 0) {
                  SingleImageComponent({ item: item })
                } else if (item.type === 1) {
                  ThreeImageComponent({ item: item })
                } else if (item.type === 2) {
                  VideoComponent({ item: item })
                }
              }
              // ...
            }
          }, (item: ItemData) => item.id.toString())
        }
        // ...
      }
      // ...
    }
    // ...
  }
}

@Component
struct SingleImageComponent{
  // ...
   build() {
    Column() {
      TopView({ item: item }).reuseId('top_id')
      MiddleSingleImageView({ item: item }).reuseId('middle_image_id')
      BottomView({ item: item }).reuseId('bottom_id')
    }
  }
}

@Component
struct ThreeImageComponent{
  // ...
   build() {
    Column() {
      TopView({ item: item }).reuseId('top_id')
      MiddleThreeImageView({ item: item }).reuseId('middle_three_image_id')
      BottomView({ item: item }).reuseId('bottom_id')
    }
  }
}

@Component
struct VideoComponent{
  // ...
   build() {
    Column() {
      TopView({ item: item }).reuseId('top_id')
      MiddleVideoView({ item: item }).reuseId('middle_video_id')
      BottomView({ item: item }).reuseId('bottom_id')
    }
  }
}

@Reusable
@Component
struct TopView {
  // ...
}

@Reusable
@Component
struct BottomView {
  // ...
}

@Reusable
@Component
struct MiddleSingleImageView {
  // ...
}

@Reusable
@Component
struct MiddleThreeImageView {
  // ...
}

@Reusable
@Component
struct MiddleVideoView {
  // ...
}

Component Reuse Between Multiple Lists

[h2]Description

In app development, there are often cases where data is displayed under different tabs, each containing its own list. If the list items across tabs share the same structure, component reuse can be applied for optimization. For example, as shown below, the News and Hot tabs display list items of the same type.

[h2]Implementation

In ArkUI, this tab-based UI can be implemented a Swiper+List layout, where each page within the Swiper contains a List to display content. According to the reuse mechanism of @Reusable, the reuse cache pool is maintained within the same parent component. However, in this case, the parent of each list item is the List component of the current page. When users switch pages within the Swiper, list items from the previous page cannot be directly reused.

In this case, you can customize a global reuse cache pool NodePool and use the node reuse capability of BuilderNode to create, recycle, and reuse child components based on the page status, implementing component reuse across multiple lists on different pages.

Reason for using the Swiper+List layout instead of the Tabs+List layout:

Currently, the content pages within the Tabs component do not support LazyForEach(), requiring use of ForEach with TabContent instead. This implementation approach forces immediate creation of all TabContent instances during initial rendering, regardless of their active state. In addition, switching between TabContent pages does not trigger the aboutToDisappear() lifecycle callback, meaning components cannot be properly reused.

In the position where a custom component is required, use NodeContainer as a placeholder, and then inherit NodeController to implement the NodeItem node class. The class needs to hold the BuilderNode instance to create and reuse nodes, and also needs to hold the data object of the view to update the UI display.

  1. When the NodeItem is about to be destroyed as the view component is about to disappear, the aboutToDisappear() method should be used to recycle the NodeItem into the NodePool cache, storing it in the collection corresponding to the type of the node.
  2. Each time a custom component needs to be created, the system first attempts to find the corresponding NodeItem by type. If no matching NodeItem ist found, a new NodeItem is created.
  3. As the view component follows the lifecycle of the NodeContainer, the data is updated and the component reuse process is completed.

This approach requires you to manually maintain the reuse pool. You can also consider using third-party libraries like nodepool, which implement the same principles for global component reuse pools.

[h2]How to Develop

  1. Implement a list item placeholder node class, NodeItem, that inherits from NodeController, and implement the makeNode() method. This method determines whether to create a node or refresh the existing data based on whether the node is available. Implement the aboutToDisappear() lifecycle callback to properly recycle the component node when it becomes inactive.

    export class NodeItem extends NodeController {
      public builder: WrappedBuilder<ESObject> | null = null;
      public node: BuilderNode<ESObject> | null = null;
      public data: ESObject = {};
      public type: string = '';
      public id: number = 0;
    
      aboutToRecycle(): void {
        NodePool.getInstance().recycleNode(this.type, this);
      }
    
      update(data: ESObject) {
        this.data = data;
        this.node?.reuse(data);
      }
    
      makeNode(uiContext: UIContext): FrameNode | null {
        if (!this.node) {
          this.node = new BuilderNode(uiContext);
          this.node.build(this.builder, this.data);
        } else {
          this.update(this.data);
        }
        return this.node.getFrameNode();
      }
    
      // ...
    }
  2. The NodePool tool class is used to implement the cache pool reuse in singleton mode, and the reuse logic of components is managed in a unified manner in the application. The getNode() method is implemented to obtain the corresponding NodeItem based on the input type. If the corresponding NodeItem is not found, a new NodeItem is created and data is bound to it. The recycleNode() method is implemented to store the corresponding NodeItem in the set based on the type.

    export class NodePool {
      private static instance: NodePool;
      private idGen: number;
      private nodePool: HashMap<string, LinkedList<NodeItem>>;
    
      private constructor() {
        this.nodePool = new HashMap();
        this.idGen = 0;
      }
    
      public static getInstance() {
        if (!NodePool.instance) {
          NodePool.instance = new NodePool();
        }
        return NodePool.instance;
      }
    
      public getNextId(): number {
        this.idGen += 1;
        return this.idGen;
      }
    
      public getNode(type: string, item: ESObject,
        builder: WrappedBuilder<ESObject>): NodeItem | undefined {
        let nodeItem: NodeItem | undefined = undefined;
        try {
          if (this.nodePool.get(type)) {
            for (let i = 0; i < this.nodePool.get(type)?.length; i++) {
              let tmpItem: NodeItem | undefined = this.nodePool.get(type)?.get(i);
              if (!tmpItem.node?.getFrameNode()?.getParent()) {
                nodeItem = tmpItem;
                this.nodePool.get(type)?.removeByIndex(i);
                break;
              }
            }
          }
        } catch (e) {
          let err = e as BusinessError;
          hilog.error(DOMAIN, 'testTag', `failed code=${err.code}, message=${err.message}`);
        }
    
        if (!nodeItem) {
          nodeItem = new NodeItem();
          nodeItem.builder = builder;
          nodeItem.type = type;
          nodeItem.data.item = item;
        } else {
          nodeItem.data.item = item;
        }
        return nodeItem;
      }
    
      public recycleNode(type: string, node: NodeItem) {
        try {
          let nodeArray: LinkedList<NodeItem> = this.nodePool.get(type);
          if (!nodeArray) {
            nodeArray = new LinkedList();
            this.nodePool.set(type, nodeArray);
          }
          node.data.item = {};
          nodeArray.add(node);
        } catch (e) {
          let err = e as BusinessError;
          hilog.error(DOMAIN, 'testTag', `failed code=${err.code}, message=${err.message}`);
        }
      }
    
      // ...
    }
    • In the getNode() method, if the parent node of the found NodeItem is not empty (indicating that the node is not completely removed from the tree), you need to continue to traverse and search for the next valid NodeItem object.
    • In the recycleNode() method, the attributes of the NodeItem object need to be reset to restore the node content and avoid display exceptions caused by reuse.
  3. Encapsulate the placeholder node from Step 1 as a reusable component. In its lifecycle methods, implement the cache retrieval, recycling, and reuse logic.

    @Component
    export struct DiffListItemNode {
      @State type: string = '';
      @State item: ItemData = new ItemData('', 0);
      @State itemHeight: number = 0;
      @State builder: WrappedBuilder<ESObject> | null = null;
      private nodeItem: NodeItem = new NodeItem();
    
      aboutToAppear(): void {
        this.nodeItem = NodePool.getInstance().getNode(this.type, this.item, this.builder!)!;
      }
    
      aboutToDisappear(): void {
        if (this.nodeItem === undefined) {
          return;
        }
        this.nodeItem?.aboutToRecycle();
      }
    
      aboutToRecycle(): void {
        this.nodeItem?.node?.recycle();
      }
    
      aboutToReuse(params: ESObject): void {
        this.nodeItem?.node?.reuse(params);
      }
    
      build() {
        NodeContainer(this.nodeItem)
      }
    }
  4. Encapsulate the list item view component and export it using the listItemBuilder function.

    @Builder
    export function listItemBuilder(data: ESObject) {
      DiffListItemView({
        item: data.item
      })
    }
    
    @Component
    export struct DiffListItemView {
      // ...
    
      aboutToReuse(params: ESObject): void {
        this.item = params.item;
      }
    
      build() {
        Row() {
          // ...
        }
        // ...
      }
    }
  5. In LazyForEach() of the list, wrap the actual list item view component from Step 4 with wrapBuilder, and pass it as a parameter to the placeholder component created in Step 3. This completes the layout with reusable components.

    export const listItemWrapper: WrappedBuilder<ESObject> = wrapBuilder<ESObject>(listItemBuilder);
    
    @Component
    export struct TabContentView {
      // ...
    
      build() {
        List() {
          LazyForEach(this.dataSource, (item: ItemData) => {
            DiffListItemNode({
              type: REUSE_VIEW_TYPE_ITEM,
              item: item,
              builder: listItemWrapper
            })
          }, (item: ItemData) => item.id.toString())
        }
        // ...
      }
    }

[h2]Using onIdle() for Pre-creating Components

The current implementation faces potential performance issues during initial page loading because the component reuse pool contains no cached elements at this stage, requiring all list items to be created from scratch. To address this bottleneck, you can implement component pre-creation to populate the reuse cache pool in advance.

However, when there are numerous components, pre-creating them all at once can be time-consuming, which could block the main thread. ArkUI provides the onIdle() API, which returns the idle time at the end of each frame. You can distribute the pre-creation of components to the idle time at the end of each frame. In this way, the pre-creation process is executed in multiple periods, avoiding the impact of time-consuming centralized running and optimizing the application experience.

  1. You must accurately estimate the component pre-creation duration and break down the service logic into smaller tasks. This allows these tasks to be distributed across multiple onIdle() calls. For example, if the pre-creation of a single component takes about 2 ms and the idle time at the frame trailer is only 1 ms, the pre-creation cannot be performed in the current frame. Instead, it is delayed to the next frame.
  2. The number of pre-created components in the custom component reuse pool must be properly controlled. Excessive pre-creation may lead to high memory usage that could degrade the overall app performance.
  1. Implement the pre-creation method preBuild() in the NodePool utility class: Create a NodeItem instance, set its properties such as builder, and call recycleNode() to pre-store it in the cache pool.

    export class NodeItem extends NodeController {
      // ...
    
      prebuild(uiContext: UIContext) {
        this.node = new BuilderNode(uiContext);
        this.node.build(this.builder, this.data);
      }
    }
    
    export class NodePool {
      // ...
    
      public preBuild(type: string, item: ESObject, builder: WrappedBuilder<ESObject>, uiContext: UIContext) {
        if (type) {
          let nodeItem: NodeItem | undefined = new NodeItem();
          nodeItem.builder = builder;
          nodeItem.data.item = item;
          nodeItem.type = type;
          nodeItem.prebuild(uiContext);
          this.recycleNode(type, nodeItem);
        }
      }
    }
  2. Inherit from FrameCallback to implement a frame callback class. Pass the pre-creation data into the constructor and implement the onIdle() API.

    1. The system call onIdle(), passing the idle time at the end of the frame through the idleTimeInNano parameter. You can set the maximum remaining idle time for pre-creation based on the time required to pre-create a single component (the example code assumes a single component pre-creation takes a maximum of 1 ms = 1000000 ns).
    2. When the remaining idle time is sufficient to create a component, the component is pre-created in the current frame, and the remaining idle time of the current frame is continuously updated.
    3. If the remaining idle time of the current frame is insufficient for creating a component, the postFrameCallback() method is used to pass the frame callback to the next frame and continue to pre-create the remaining components.
      export class IdleCallback extends FrameCallback {
        private uiContext: UIContext;
        private todoCount: number = 0;
        private dataArray: ItemData[] = [];
      
        constructor(context: UIContext, preBuildData: ItemData[]) {
          super();
          this.uiContext = context;
          this.dataArray = preBuildData;
        }
      
        onIdle(idleTimeInNano: number): void {
          if (this.todoCount >= this.dataArray.length) {
            return;
          }
          let cur: number = systemDateTime.getTime(true);
          let timeLeft = idleTimeInNano;
          while (timeLeft >= 1000000) {
            hiTraceMeter.startTrace('onIdle_prebuild', 1);
            NodePool.getInstance().preBuild('reuse_type_', this.dataArray[this.todoCount], listItemWrapper, this.uiContext);
            hiTraceMeter.finishTrace('onIdle_prebuild', 1);
            let now = systemDateTime.getTime(true);
            timeLeft = timeLeft - (now - cur);
            cur = now;
            this.todoCount++;
            if (this.todoCount >= this.dataArray.length) {
              return;
            }
            if (this.todoCount < this.dataArray.length) {
              this.uiContext.postFrameCallback(this);
            }
          }
        }
      }
  3. Before accessing the Swiper+List page, choose an appropriate timing to call context.postFrameCallback() to trigger the IdleCallback frame callback logic.

    @Entry
    @Component
    struct Index {
      // ...
    
      aboutToAppear(): void {
        let dataArray: ItemData[] = [];
        dataArray.push(...genMockItemData(100))
        let context = this.getUIContext();
        context.postFrameCallback(new IdleCallback(context, dataArray));
      }
    
      // ...
    }

More Optimization Methods

[h2]Using attributeUpdater for Partial Component Attribute Updates

You can use attributeUpdater in reusable components to control the update of specified attributes, avoiding unnecessary repainting and reducing the rendering load, thereby improving the application performance.

By default, directly assigning state variables in aboutToReuse() triggers a full refresh of the component's attributes. However, real-world scenarios often require refreshing only specific attributes. For example, when reusing a component, you may only need to modify the text color without affecting other attributes. In this case, you can use attributeUpdater to modify the fontColor attribute in aboutToReuse().

Negative example:

In this example, updating the fontColor state variable in aboutToReuse() causes all the component's attributes to be refreshed, resulting in unnecessary time consumption.

@Component
export struct LessEmbeddedComponent {
  aboutToAppear(): void {
    momentData.getFriendMomentFromRawfile();
  }

  build() {
    Column() {
      Text('use nothing')
      List({ space: ListConstants.LIST_SPACE }) {
        LazyForEach(momentData, (moment: FriendMoment) => {
          ListItem() {
            OneMomentNoModifier({ color: moment.color })
              .onClick(() => {
                
              })
          }
        }, (moment: FriendMoment) => moment.id)
      }
      .width("100%")
      .height("100%")
      .cachedCount(5)
    }
  }
}

@Reusable
@Component
export struct OneMomentNoModifier {
  @State color: string | number | Resource = "";

  aboutToReuse(params: Record<string, Object>): void {
    this.color = params.color as number;
  }

  build() {
    Column() {
      Text('This is title')
      Text('This is desc text')
        .fontColor(this.color)
        .textAlign(TextAlign.Center)
        .fontStyle(FontStyle.Normal)
        .fontSize(13)
        .lineHeight(30)
        .opacity(0.6)
        .margin({ top: 10 })
        .fontWeight(30)
        .clip(false)
        .backgroundBlurStyle(BlurStyle.NONE)
        .foregroundBlurStyle(BlurStyle.NONE)
        .borderWidth(1)
        .borderColor(Color.Pink)
        .borderStyle(BorderStyle.Solid)
        .alignRules({
          'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
          'left': { 'anchor': 'image', 'align': HorizontalAlign.End }
        })
    }
  }
}

Positive example:

Use attributeUpdater to precisely refresh the fontColor attribute in the Text component, avoiding re-rendering of unchanged attributes.

export class MyTextUpdater extends AttributeUpdater<TextAttribute> {
  private color: string | number | Resource | Color = '';

  constructor(color: string | number | Resource | Color) {
    super();
    this.color = color;
  }

  initializeModifier(instance: TextAttribute): void {
    instance.fontColor(this.color)
  }
}

// ...

@Reusable
@Component
export struct OneMomentNoModifier {
  @State text: string = '';
  color: string | number | Resource | Color = '';
  textUpdater: MyTextUpdater | null = null;

  aboutToAppear(): void {
    this.textUpdater = new MyTextUpdater(this.color);
  }

  aboutToReuse(params: Record<string, Object>): void {
    this.color = params.color as string;
    this.text = params.text as string;
    this.textUpdater?.attribute?.fontColor(this.color);
  }

  build() {
    Column() {
      Text(this.text)
        .fontSize(18)
        .textAlign(TextAlign.Center)
        .fontWeight(500)
        .lineHeight(24)
        .fontColor(Color.Black)
        .opacity(0.6)
      Column() {
        Text('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
          .attributeModifier(this.textUpdater)
          .textAlign(TextAlign.Start)
          .fontSize(16)
          .fontWeight(400)
          .lineHeight(21)
      }
      // ...
    }
    // ...
  }
}

[h2]Avoiding Repeated Assignment of Automatically Updated State Variables in aboutToReuse()

If a reusable component uses state variables such as @Link/@ObjectLink/@Prop that automatically synchronize data between parent and child components, you do not need to assign values to these variables again in aboutToReuse(). Reassigning values to these variables will cause the component's content to trigger a state update, resulting in additional computation and update delays.

Negative example:

@Reusable
@Component
export struct OneMoment {
  @ObjectLink moment: FriendMoment;

  aboutToReuse(params: Record<string, Object>): void {
    this.moment.id = (params.moment as FriendMoment).id
    this.moment.userName = (params.moment as FriendMoment).userName
    this.moment.avatar = (params.moment as FriendMoment).avatar
    this.moment.text = (params.moment as FriendMoment).text
    this.moment.image = (params.moment as FriendMoment).image
  }

  build() {
    Column() {
      ...
      Text(`${this.moment.userName}`)
      ...
    }
  }
}

Positive example:

Delete the assignment statement from aboutToReuse().

The state variable moment decorated with @ObjectLink can be automatically updated and does not need to be updated again.

[h2]Using reuseId to Mark Components with Layout Changes

In the same custom component code, using if/else conditional statements to control the layout structure can lead to the creation of different components in different logical branches. This results in variations in the component tree structure. In such cases, you can use reuseId to distinguish between the changed branches. This ensures that the system can cache components with different layouts based on their reuseId, thereby improving reuse performance.

Negative example:

A component is created with a Flex container that includes an Image element based on an if condition. Without using reuseId, after reuse, the Flex component might be deleted or re-created based on the if condition, leading to performance overhead.

@Entry
@Component
struct AboutReuseId {
  
  build() {
    Column() {
      TopBar()
      List({ space: ListConstants.LIST_SPACE }) {
        LazyForEach(momentData, (moment: FriendMoment) => {
          ListItem() {
            OneMoment({
              moment: moment,
              fontSize: moment.size
            })
          }
        }, (moment: FriendMoment) => moment.id)
      }
      .cachedCount(Constants.CACHED_COUNT)
    }
  }
}

@Reusable
@Component
export struct OneMoment {
  @Prop moment: FriendMoment;

  build() {
    Column() {
      ...
      Text(this.moment.text)

      if (this.moment.image !== '') {
        Flex({ wrap: FlexWrap.Wrap }) {
          Image($r(this.moment.image))
          Image($r(this.moment.image))
          Image($r(this.moment.image))
        }
      }
      ...
    }
  }
}

Positive example:

Set different reuseId values based on the branching logic to cache components for different layout structures, eliminating the repetitive execution of conditional deletion or creation operations.

@Entry
@Component
struct WithReuseId {
  // ...

  build() {
    Column() {
      List({ space: this.LIST_SPACE }) {
        LazyForEach(this.momentData, (moment: FriendMoment) => {
          ListItem() {
            OneMoment({ moment: moment })
              .reuseId((moment.image !== '') ? 'withImage_id' : 'noImage_id')
          }
        }, (moment: FriendMoment) => moment.id)
      }
      // ...
    }
  }
}

@Reusable
@Component
export struct OneMoment {
  @ObjectLink moment: FriendMoment;

  build() {
    Column() {
      // ...

      if (this.moment.image !== '') {
        Flex({ wrap: FlexWrap.Wrap }) {
          Image($r(this.moment.image))
            .width(Constants.LAYOUT_MAX)
            .height('27.5%')
            .borderRadius(16)
          Image($r(this.moment.image))
            .width(Constants.LAYOUT_MAX)
            .height('27.5%')
            .borderRadius(16)
            .margin({ top: 10 })
        }
        .width(Constants.LAYOUT_MAX)
        .margin({ top: 14 })
      }
    }
    // ...
  }
}

[h2]Avoiding Function Parameters in Reusable Components

If a reusable component takes a function as a parameter, the associated data object for the component needs to be re-created each time the component is reused. As a result, the function is executed during each reuse, which can lead to performance issues. To reduce the performance overhead caused by repeatedly executing the function parameter, pass parameters through state variables.

Negative example:

The sum parameter for a reused child component is generated by a time-consuming function countAndReturn(). This function is executed every time the component is reused, causing performance issues and even frame drops during the list scrolling process.

@Entry
@Component
struct withFuncParam {
  aboutToAppear(): void {
    getFriendMomentFromRawfile();
  }

  countAndReturn(): number {
    let temp: number = 0;
    for (let index = 0; index < 100000; index++) {
      temp += index;
    }
    return temp;
  }

  build() {
    Column() {
      TopBar()
      List({ space: ListConstants.LIST_SPACE }) {
        LazyForEach(momentData, (moment: FriendMoment) => {
          ListItem() {
            OneMoment({
              moment: moment,
              sum: this.countAndReturn()
            })
          }
        }, (moment: FriendMoment) => moment.id)
      }
      .cachedCount(Constants.CACHED_COUNT)
    }
  }
}

@Reusable
@Component
export struct OneMoment {
  @Prop moment: FriendMoment;
  @State sum: number = 0;

  aboutToReuse(params: Record<string, Object>): void {
    this.sum = params.sum as number;
  }

  build() {
    Column() {
      ...
      Text(`${this.moment.userName} (${this.moment.id} / ${this.sum})`)
      ...
    }
  }
}

Positive example:

You can calculate countAndReturn() once during the initial page load and assign the result to the this.sum variable. Then, when passing parameters to the reusable component, use this.sum.

@Entry
@Component
struct WithFuncParam {
  @State sum: number = 0;
  // ...
  private readonly MOCK_ASYNC_DEFAULT_NUM: number = 1000000;
  private readonly MOCK_ASYNC_TIME_OUT_NUM: number = 2000;

  aboutToAppear(): void {
    this.momentData.getFriendMomentFromRawFile();
    this.countAndReturn();
  }

  async countAndReturn(): Promise<void> {
    await this.sleep();
    this.sum = this.MOCK_ASYNC_DEFAULT_NUM;
  }

  sleep(): Promise<string> {
    return new Promise<string>((resolve) => {
      setTimeout(() => {
        resolve('ok');
      }, this.MOCK_ASYNC_TIME_OUT_NUM)
    });
  }

  build() {
    Column() {
      List({ space: this.LIST_SPACE }) {
        LazyForEach(this.momentData, (moment: FriendMoment) => {
          ListItem() {
            OneMoment({
              moment: moment,
              sum: this.sum
            })
          }
        }, (moment: FriendMoment) => moment.id)
      }
      // ...
    }
  }
}

@Reusable
@Component
export struct OneMoment {
  @ObjectLink moment: FriendMoment;
  @State sum: number = 0;
  // ...

  aboutToReuse(params: Record<string, Object>): void {
    this.sum = params.sum as number;
  }

  // ...
}

FAQs

[h2]How do I check whether component reuse is effective?

Sample Code