ArkUI 高性能列表懒加载与组件复用实战

前言

在 HarmonyOS 应用里,列表是最常见也最容易卡顿的界面。很多开发者一上来用 ForEach 把成百上千条数据一次性塞进 List,结果首屏白屏、滑动掉帧、内存飙升。本文基于华为开发者论坛的真实问题「ArkUI 自定义组件如何实现高性能的列表懒加载与复用」,系统讲解 LazyForEach + cachedCount + @Reusable 这套官方高性能组合,并给出可运行的完整示例。

问题描述

  • 使用 ForEach 渲染大列表时,所有子组件在首屏就被创建,节点数随数据量线性暴涨;
  • 滑动时频繁创建/销毁组件,触发大量布局与渲染,造成掉帧;
  • 即使数据不变,滚动时也看不到明显的性能改善。

根本原因是:没有懒加载 + 没有组件复用。ArkUI 的 ForEach 是"全量展开",而 LazyForEach 才是"按需创建"。

细节解析

1. LazyForEach:只创建视口内的节点

LazyForEach 接收一个 IDataSource,只在组件进入视口附近时才创建节点,离开后销毁。它要求第二项参数是组件工厂,第三项参数是唯一 key 生成函数(必须稳定,否则复用错乱)。

2. cachedCount:视口外预创建

ListcachedCount 控制视口上下额外预创建的节点数。设太小滑动会"现创建"导致卡顿,设太大又吃内存,一般取单屏可见数的 1~2 倍(3~8)。

3. @Reusable:组件复用而不是重建

@Reusable 装饰的自定义组件在被滑出视口后不会销毁,而是进入复用池;再次进入视口时调用 aboutToReuse(params) 更新数据后直接使用。这是滑动流畅的关键。

4. aboutToReuse:复用时的数据刷新

复用组件不会重新走 aboutToAppear,必须用 aboutToReuse 把新数据写回状态变量,否则会显示旧内容。

示例代码

// 数据源:必须实现 IDataSource
export class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private dataArray: number[] = [];
  totalCount(): number { return this.dataArray.length; }
  getData(index: number): number { return this.dataArray[index]; }
  push(data: number): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) this.listeners.push(listener);
  }
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) this.listeners.splice(pos, 1);
  }
  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => listener.onDataAdd(index));
  }
}

// 可复用列表项
@Reusable
@Component
struct ReusableListItem {
  @State item: number = 0;
  // 复用入池前/后由框架调用,用于刷新数据
  aboutToReuse(params: Record<string, Object>): void {
    this.item = params.item as number;
  }
  build() {
    Row() {
      Image($r('app.media.startIcon')).width(40).height(40)
      Text('Item #' + this.item)
        .fontSize(16)
        .layoutWeight(1)
        .margin({ left: 12 })
    }
    .width('100%')
    .height(64)
    .padding({ left: 16, right: 16 })
  }
}

@Entry
@Component
struct HighPerfListPage {
  private data: BasicDataSource = new BasicDataSource();
  private scroller: ListScroller = new ListScroller();

  aboutToAppear(): void {
    for (let i = 0; i < 1000; i++) this.data.push(i);
  }

  build() {
    Column() {
      List({ scroller: this.scroller }) {
        LazyForEach(this.data, (item: number) => {
          ListItem() {
            ReusableListItem().item(item)   // 注意:用 aboutToReuse 接收
          }
        }, (item: number) => item.toString())  // 稳定的唯一 key
      }
      .width('100%')
      .layoutWeight(1)
      .cachedCount(5)   // 视口外预创建 5 个节点
    }
  }
}

提示:上面 ReusableListItem().item(item) 写法需配合 @ReusableaboutToReuse;若用构造参数传值,框架会在复用时自动以 aboutToReuse 注入新参数。

总结

  • 大列表一律用 LazyForEach 代替 ForEach,节点数从"全量"降为"视口+缓存";
  • cachedCount 是滑动流畅度的调节旋钮,按单屏高度取值 3~8;
  • @Reusable + aboutToReuse 让组件"复用而非重建",是掉帧的根治手段;
  • key 生成函数必须稳定唯一,否则会出现数据错位。

按此组合,千级列表也能稳定 60fps。下一篇我们将聚焦「深色模式优雅适配」,敬请关注。

Logo

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

更多推荐