列表一卡,用户就骂你 App 垃圾

列表是移动端最常见的 UI,也是最容易翻车的。数据少的时候没事,一上千条,滑动就开始掉帧,图片加载不出来,内存飙升。用户不会管你代码写得怎么样,他只看到“这 App 好卡”。

很多人第一反应是“用 LazyForEach 不就行了?” 对,但 LazyForEach 只是第一步。如果你不配合组件复用、图片缓存、数据更新策略,该卡还是卡。

这篇就讲清楚:LazyForEach 怎么用、@Reusable 怎么复用、数据更新怎么不闪屏,以及那些让你滑动掉帧的隐藏坑。

https://placeholder-01.jpg/

一、LazyForEach:别再用 ForEach 硬扛

ForEach 会一次性创建所有列表项,数据一多,内存和渲染压力都爆炸。LazyForEach 则是按需创建,滑到哪渲染到哪。

但 LazyForEach 不能直接传数组,必须传一个实现了 IDataSource 接口的对象:

typescript

class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private dataArray: string[] = [];

  totalCount(): number {
    return this.dataArray.length;
  }

  getData(index: number): string {
    return this.dataArray[index];
  }

  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);
    }
  }

  // 数据更新方法
  public addData(item: string): void {
    this.dataArray.push(item);
    this.listeners.forEach(listener => listener.onDataAdd(this.dataArray.length - 1));
  }

  public deleteData(index: number): void {
    this.dataArray.splice(index, 1);
    this.listeners.forEach(listener => listener.onDataDelete(index));
  }

  public changeData(index: number, item: string): void {
    this.dataArray[index] = item;
    this.listeners.forEach(listener => listener.onDataChange(index));
  }
}

然后在组件里用:

typescript

@Entry
@Component
struct LongListPage {
  private dataSource: BasicDataSource = new BasicDataSource();

  aboutToAppear() {
    for (let i = 0; i < 10000; i++) {
      this.dataSource.addData(`Item ${i}`);
    }
  }

  build() {
    List() {
      LazyForEach(this.dataSource, (item: string) => {
        ListItem() {
          Text(item)
            .fontSize(16)
            .padding(16)
        }
      }, (item: string) => item)
    }
    .width('100%')
    .height('100%')
    .cachedCount(5) // 预加载5个
  }
}

cachedCount 控制预加载数量,默认是 1。设大一点滑动更流畅,但内存占用也更高。一般设 3-5 比较合适。

https://placeholder-02.png/

二、@Reusable:组件复用才是性能关键

LazyForEach 解决了“创建多少”的问题,但每次滑动还是会重建组件。如果列表项结构复杂,重建开销很大。

这时候就要用 @Reusable 装饰器,让组件可以被复用:

typescript

@Reusable
@Component
struct ListItemComponent {
  @State item: string = '';

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

  build() {
    Row() {
      Image($r('app.media.icon'))
        .width(40)
        .height(40)
      Text(this.item)
        .fontSize(16)
        .margin({ left: 12 })
    }
    .padding(16)
  }
}

然后在 LazyForEach 里用:

typescript

LazyForEach(this.dataSource, (item: string) => {
  ListItem() {
    ListItemComponent({ item: item })
  }
}, (item: string) => item)

关键在 aboutToReuse。当组件被复用时,会调用这个方法,你在这里更新数据,而不是重新创建组件。这样滑动时就不会频繁触发 aboutToAppear 和 build,性能提升非常明显。

注意: @Reusable 必须配合 LazyForEach 使用才有效果,ForEach 不行。

三、数据更新:别动不动就全量刷新

很多人更新列表数据时,直接 this.dataSource = newDataSource,这会导致整个列表重建,滑动位置丢失,体验极差。

正确做法是用 DataChangeListener 提供的细粒度更新方法:

  • onDataAdd(index):新增一项。

  • onDataDelete(index):删除一项。

  • onDataChange(index):修改一项。

  • onDataMove(from, to):移动一项。

比如删除:

typescript

public deleteData(index: number): void {
  this.dataArray.splice(index, 1);
  this.listeners.forEach(listener => listener.onDataDelete(index));
}

这样列表只会移除对应项,不会重建整个列表。

坑来了: 如果你在 onDataChange 里传的 index 不对,列表会错乱。比如你删除了第 3 项,但通知的是第 5 项,那 UI 就会显示错误的数据。所以 index 一定要和实际数据操作一致。

https://placeholder-03.png/

四、图片加载:别让图片拖垮列表

列表里如果有图片,加载不当会非常卡。几个优化点:

第一,用 Image 的 syncLoad 属性。 默认是异步加载,滑动快的时候可能出现白块。设置 syncLoad(true) 会同步加载,但会阻塞 UI,只适合小图。

第二,设置 alt 占位图。 图片没加载出来时显示占位图,避免布局跳动:

typescript

Image(this.item.imageUrl)
  .alt($r('app.media.placeholder'))
  .width(80)
  .height(80)

第三,图片缓存。 HarmonyOS 的 Image 组件自带内存缓存,但磁盘缓存需要自己处理。可以用 @ohos.file.fs 把图片下载到沙箱,下次直接从本地读。

第四,控制图片尺寸。 别用原图直接显示,用 objectFit 配合固定宽高,避免解码大图浪费内存。

五、常见坑合集

坑一:key 生成重复。

LazyForEach 的第三个参数是 key 生成器,必须保证每条数据 key 唯一。如果你用 index 当 key,删除数据后 key 会错乱,导致复用错位。要用数据本身的唯一 ID。

坑二:嵌套 List 性能差。

List 里面再套 List,滑动时内外层都会触发渲染,性能极差。尽量用扁平化数据结构,或者用 Grid 代替。

坑三:build 里做复杂计算。

build() 会频繁执行,在里面写排序、过滤、格式化,页面必卡。所有计算提前在 aboutToAppear 或数据更新时做完。

坑四:@State 修饰大数组。

@State 会深度监听数组变化,数据量大时性能很差。用 @State 修饰 IDataSource 对象,而不是数组本身。

https://placeholder-04.jpg/

六、总结

这篇讲了长列表性能优化的核心手段:LazyForEach、@Reusable、细粒度数据更新和图片优化。核心记住三点:

  1. LazyForEach + @Reusable 是黄金组合,缺一不可。

  2. 数据更新用 onDataAdd/Delete/Change,别全量刷新。

  3. key 必须唯一,别用 index。

Logo

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

更多推荐