HarmonyOS 列表性能优化:@Reusable 组件复用让长列表丝滑滚动

前言

List / Grid 是几乎所有 App 的主骨架。当数据上千条、且每个 ListItem 结构复杂(图片、富文本、嵌套布局)时,简单的 ForEach 会遇到两个致命问题:频繁创建销毁组件节点导致滚动掉帧、以及状态错乱(复用后旧数据没清空)。ArkUI 提供 @Reusable 装饰器 + aboutToReuse 生命周期,把「组件节点池化复用」这件事标准化。本文结合可运行示例,讲透 @Reusable 的原理、复用回调的正确写法、以及与 @State/@ObservedV2 的配合,帮你把长列表从「一滑就卡」优化到「丝般顺滑」。

问题描述

典型症状:

  1. 列表滚动到几百项后明显掉帧,Profiler 显示「组件创建」占比极高。
  2. 复用的 ListItem 偶尔显示上一条的旧数据——因为没在复用回调里重置状态。
  3. aboutToReuse 里重新 new 了整个数据对象,反而比不复用还慢。
  4. @Reusable 加在了 List 上而不是 ListItem 内部的自定义组件上,完全没生效。

根因:ForEach 默认按「进屏创建、出屏销毁」管理节点;@Reusable 则让它「出屏回收、进屏复用」,省掉创建开销——但复用必须配合 aboutToReuse(params) 把新数据灌进来。

细节解析

1. @Reusable 作用的对象

  • 必须加在ForEach/LazyForEach 直接生成的子组件上(通常是封装好的 ListItem 内容组件),不是 List 本身。
  • 配合 LazyForEach 使用效果最佳:LazyForEach 负责「按需加载数据」,@Reusable 负责「节点复用」,两者是正交优化。

2. aboutToReuse 生命周期

  • 组件从复用池取出、重新入屏前调用 aboutToReuse(params: object),参数来自 reuseId + 你 this.item = params 注入的数据。
  • 这里是重置/更新状态的唯一正确位置,相当于「二次初始化」。
  • 不要在这里 new 大对象,直接把传入的字段赋值给 @Trace/@State 即可,框架会自动触发最小刷新。

3. reuseId 分组

  • 列表里有多种「布局样式」时,用 reuseId 区分,避免把 A 样式节点复用成 B 样式。
  • 同一 reuseId 的节点可互相复用;不同 reuseId 进不同池。

4. 与状态管理 V2 的配合

  • 组件内用 @Trace 标记会被复用时变化的字段,aboutToReuse 里直接赋值,刷新粒度精确到字段。

示例代码(可运行 ArkTS/ArkUI)

示例 1:基础 @Reusable 列表项

// components/FeedItem.ets
@Reusable
@ComponentV2
struct FeedItem {
  @Trace title: string = '';
  @Trace cover: ResourceStr = $r('app.media.startIcon');
  @Trace liked: boolean = false;

  // 复用入屏前,框架用新数据调用此回调
  aboutToReuse(params: Record<string, Object>): void {
    this.title = params['title'] as string;
    this.cover = params['cover'] as ResourceStr;
    this.liked = params['liked'] as boolean;
  }

  build() {
    Row({ space: 12 }) {
      Image(this.cover).width(64).height(64).borderRadius(8)
      Column({ space: 4 }) {
        Text(this.title).fontSize(16).maxLines(2)
        Text(this.liked ? '已赞' : '未赞').fontColor(this.liked ? '#0A59F7' : '#999')
      }.alignItems(HorizontalAlign.Start)
    }
    .padding(12)
    .width('100%')
  }
}

示例 2:用 LazyForEach + 数据源驱动

// model/FeedDataSource.ets
import { BasicDataSource } from './BasicDataSource';

export class FeedDataSource extends BasicDataSource<Feed> {
  // 继承官方 BasicDataSource,实现 getData/getCount/... 即可
}

export interface Feed {
  id: number;
  title: string;
  cover: ResourceStr;
  liked: boolean;
}
// pages/FeedList.ets
import { FeedItem } from '../components/FeedItem';
import { FeedDataSource, Feed } from '../model/FeedDataSource';

@Entry
@ComponentV2
struct FeedList {
  private data: FeedDataSource = new FeedDataSource();

  aboutToAppear() {
    for (let i = 0; i < 1000; i++) {
      this.data.pushData({ id: i, title: `动态 ${i}`, cover: $r('app.media.startIcon'), liked: false });
    }
  }

  build() {
    List() {
      LazyForEach(this.data, (item: Feed) => {
        ListItem() {
          FeedItem()
            .reuseId('feed') // 同一布局样式共用复用池
        }
      }, (item: Feed) => item.id.toString())
    }
    .cachedCount(5) // 预渲染前后各 5 个,进一步减少白屏
  }
}

示例 3:多布局用 reuseId 分组

LazyForEach(this.data, (item: any) => {
  ListItem() {
    if (item.type === 'text') {
      TextItem().reuseId('text')
    } else {
      ImageItem().reuseId('image')
    }
  }
}, (item) => item.id.toString())

总结

  • @Reusable 加在列表项组件上,不是 List 上;配合 LazyForEach 效果最大化。
  • aboutToReuse(params) 是状态重置的唯一正确入口,把新数据灌进 @Trace 字段,框架自动最小刷新。
  • 不要在其中 new 大对象,直接赋值即可,否则抵消复用收益。
  • 多布局用 reuseId 分组,避免样式串台。
  • cachedCount 配合调优:缓存前后 N 个节点,减少滚动白屏与瞬时创建。
  • 验证方法:开启 DevEco Profiler 的「组件创建」帧,开启 @Reusable 后该指标应显著下降,滚动 FPS 稳定贴近 60/120。

@Reusable 是长列表从「能滚」到「流畅」的分水岭。它改动极小(加一个装饰器 + 一个回调),收益却贯穿整个列表体验,是所有信息流、订单列表、聊天记录的必选项。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐