HarmonyOS 鸿蒙Next中在应用展示大量数据时如何避免卡顿?

HarmonyOS 鸿蒙Next中在应用展示大量数据时如何避免卡顿? 在鸿蒙应用中展示大量数据时,如何避免卡顿? ForEach和LazyForEach有什么区别? 如何实现高性能的列表滚动?

4 回复

官方文档解释得挺清楚的呀,就是全量加载和按需加载的区别

更多关于HarmonyOS 鸿蒙Next中在应用展示大量数据时如何避免卡顿?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


实现代码

/**
 * 数据源实现 - LazyForEach必需
 */
class RecordDataSource implements IDataSource {
  private records: HumanRecord[] = [];
  private listeners: DataChangeListener[] = [];

  constructor(records: HumanRecord[]) {
    this.records = records;
  }

  // 获取数据总数
  totalCount(): number {
    return this.records.length;
  }

  // 获取指定位置的数据
  getData(index: number): HumanRecord {
    return this.records[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);
    }
  }

  // 通知数据重新加载
  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    });
  }

  // 通知数据添加
  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
    });
  }

  // 通知数据改变
  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index);
    });
  }

  // 通知数据删除
  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
    });
  }

  // 添加数据
  public addData(record: HumanRecord): void {
    this.records.push(record);
    this.notifyDataAdd(this.records.length - 1);
  }

  // 更新数据
  public updateData(index: number, record: HumanRecord): void {
    this.records[index] = record;
    this.notifyDataChange(index);
  }

  // 删除数据
  public deleteData(index: number): void {
    this.records.splice(index, 1);
    this.notifyDataDelete(index);
  }

  // 重新加载数据
  public reloadData(records: HumanRecord[]): void {
    this.records = records;
    this.notifyDataReload();
  }
}

/**
 * 记录列表页面 - 使用LazyForEach优化
 */
@Entry
@Component
struct RecordListPage {
  @State recordDataSource: RecordDataSource = new RecordDataSource([]);
  @State loading: boolean = true;
  @State primaryColor: string = '#FA8C16';
  
  private dataService: DataService = DataService.getInstance();
  private scroller: Scroller = new Scroller();

  aboutToAppear() {
    this.loadData();
    this.loadThemeColor();
  }

  /**
   * 加载数据
   */
  private async loadData() {
    try {
      this.loading = true;
      const records = await this.dataService.getAllRecords(
        undefined, 
        { field: 'eventTime', order: 'desc' }
      );
      
      // 更新数据源
      this.recordDataSource.reloadData(records);
      this.loading = false;
    } catch (error) {
      console.error('加载数据失败:', JSON.stringify(error));
      this.loading = false;
    }
  }

  /**
   * 加载主题颜色
   */
  private loadThemeColor() {
    this.primaryColor = ThemeConstants.getPrimaryColor();
  }

  build() {
    Column() {
      // 导航栏
      this.buildHeader()

      if (this.loading) {
        // 加载中
        this.buildLoadingView()
      } else {
        // 列表内容
        this.buildListView()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  /**
   * 构建导航栏
   */
  @Builder
  buildHeader() {
    Row() {
      Image($r('app.media.ic_back'))
        .width(24)
        .height(24)
        .fillColor('#FFFFFF')
        .onClick(() => router.back())

      Text('人情记录')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .margin({ left: 16 })
    }
    .width('100%')
    .height(60)
    .padding({ left: 20, right: 20 })
    .backgroundColor(this.primaryColor)
  }

  /**
   * 构建列表视图 - 使用LazyForEach
   */
  @Builder
  buildListView() {
    List({ scroller: this.scroller }) {
      // 使用LazyForEach实现懒加载
      LazyForEach(
        this.recordDataSource,
        (record: HumanRecord, index: number) => {
          ListItem() {
            this.buildRecordItem(record, index)
          }
          .swipeAction({ end: this.buildSwipeAction(record, index) })
        },
        (record: HumanRecord) => record.id
      )
    }
    .width('100%')
    .layoutWeight(1)
    .edgeEffect(EdgeEffect.Spring)
    .divider({
      strokeWidth: 1,
      color: '#F0F0F0',
      startMargin: 16,
      endMargin: 16
    })
    .cachedCount(3)  // 缓存3个item,提升滚动性能
    .friction(0.6)   // 设置摩擦系数
  }

  /**
   * 构建记录项
   */
  @Builder
  buildRecordItem(record: HumanRecord, index: number) {
    Row() {
      // 左侧图标
      Column() {
        Text(this.getEventTypeIcon(record.eventType))
          .fontSize(24)
      }
      .width(50)
      .height(50)
      .backgroundColor(this.getEventTypeColor(record.eventType) + '20')
      .borderRadius(25)
      .justifyContent(FlexAlign.Center)
      .margin({ right: 12 })

      // 中间信息
      Column() {
        Row() {
          Text(record.personName || '未知')
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .fontColor('#262626')
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
          
          Text(this.getEventTypeName(record.eventType))
            .fontSize(12)
            .fontColor('#8C8C8C')
            .margin({ left: 8 })
            .padding({ left: 8, right: 8, top: 2, bottom: 2 })
            .backgroundColor('#F5F5F5')
            .borderRadius(4)
        }
        .width('100%')
        .margin({ bottom: 4 })

        Text(this.formatDate(record.eventTime))
          .fontSize(12)
          .fontColor('#8C8C8C')

        if (record.remark) {
          Text(record.remark)
            .fontSize(12)
            .fontColor('#595959')
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
            .margin({ top: 4 })
        }
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)

      // 右侧金额
      Column() {
        Text(record.type === 'received' ? '+' : '-')
          .fontSize(14)
          .fontColor(record.type === 'received' ? '#52C41A' : '#FF4D4F')
        
        Text(`¥${record.amount}`)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(record.type === 'received' ? '#52C41A' : '#FF4D4F')
      }
      .alignItems(HorizontalAlign.End)
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .onClick(() => {
      this.onRecordClick(record);
    })
  }

  /**
   * 构建滑动操作
   */
  @Builder
  buildSwipeAction(record: HumanRecord, index: number) {
    Row() {
      // 编辑按钮
      Button('编辑')
        .fontSize(14)
        .fontColor('#FFFFFF')
        .backgroundColor('#1890FF')
        .width(60)
        .height('100%')
        .onClick(() => {
          this.onEditRecord(record);
        })

      // 删除按钮
      Button('删除')
        .fontSize(14)
        .fontColor('#FFFFFF')
        .backgroundColor('#FF4D4F')
        .width(60)
        .height('100%')
        .onClick(() => {
          this.onDeleteRecord(record, index);
        })
    }
  }

  /**
   * 构建加载视图
   */
  @Builder
  buildLoadingView() {
    Column() {
      LoadingProgress()
        .width(40)
        .height(40)
        .color(this.primaryColor)
      
      Text('加载中...')
        .fontSize(14)
        .fontColor('#8C8C8C')
        .margin({ top: 16 })
    }
    .width('100%')
    .layoutWeight(1)
    .justifyContent(FlexAlign.Center)
  }

  /**
   * 记录点击事件
   */
  private onRecordClick(record: HumanRecord) {
    router.pushUrl({
      url: 'pages/RecordDetailPage',
      params: { recordId: record.id }
    });
  }

  /**
   * 编辑记录
   */
  private onEditRecord(record: HumanRecord) {
    router.pushUrl({
      url: 'pages/EditRecordPage',
      params: { recordId: record.id }
    });
  }

  /**
   * 删除记录
   */
  private async onDeleteRecord(record: HumanRecord, index: number) {
    try {
      await this.dataService.deleteRecord(record.id);
      this.recordDataSource.deleteData(index);
      
      promptAction.showToast({
        message: '删除成功',
        duration: 2000
      });
    } catch (error) {
      promptAction.showToast({
        message: '删除失败',
        duration: 2000
      });
    }
  }

  /**
   * 格式化日期
   */
  private formatDate(timestamp: number): string {
    const date = new Date(timestamp);
    return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
  }

  /**
   * 获取事件类型图标
   */
  private getEventTypeIcon(eventType: string): string {
    const iconMap: Record<string, string> = {
      'wedding': '🎉',
      'funeral': '🕊️',
      'birthday': '🎂',
      'full_moon': '👶',
      'graduation': '🎓',
      'moving': '🏠',
      'holiday': '🎊',
      'visit_sick': '🏥',
      'other': '📝'
    };
    return iconMap[eventType] || '📝';
  }

  /**
   * 获取事件类型名称
   */
  private getEventTypeName(eventType: string): string {
    const nameMap: Record<string, string> = {
      'wedding': '婚礼',
      'funeral': '丧礼',
      'birthday': '生日',
      'full_moon': '满月',
      'graduation': '升学',
      'moving': '乔迁',
      'holiday': '节日',
      'visit_sick': '探病',
      'other': '其他'
    };
    return nameMap[eventType] || '其他';
  }

  /**
   * 获取事件类型颜色
   */
  private getEventTypeColor(eventType: string): string {
    const colorMap: Record<string, string> = {
      'wedding': '#FF6B6B',
      'funeral': '#4ECDC4',
      'birthday': '#45B7D1',
      'full_moon': '#96CEB4',
      'graduation': '#FFEAA7',
      'moving': '#DDA0DD',
      'holiday': '#FFB347',
      'visit_sick': '#87CEEB',
      'other': '#D3D3D3'
    };
    return colorMap[eventType] || '#D3D3D3';
  }
}

ForEach vs LazyForEach

ForEach (一次性渲染)

List() {
  ForEach(records, (record: HumanRecord) => {
    ListItem() {
      // 所有item一次性创建
    }
  })
}

问题:

  • ❌ 数据量大时卡顿
  • ❌ 内存占用高
  • ❌ 首屏加载慢

LazyForEach (按需渲染)

List() {
  LazyForEach(dataSource, (record: HumanRecord) => {
    ListItem() {
      // 只创建可见的item
    }
  }, (record: HumanRecord) => record.id)
}

优势:

  • ✅ 按需加载,性能好
  • ✅ 内存占用低
  • ✅ 支持大数据量

性能优化关键参数

1. cachedCount

List() {
  // ...
}
.cachedCount(3)  // 缓存3个item

作用: 缓存屏幕外的item数量 建议值: 3-5

2. friction

List() {
  // ...
}
.friction(0.6)  // 摩擦系数

作用: 控制滑动阻力 建议值: 0.6-0.9

3. edgeEffect

List() {
  // ...
}
.edgeEffect(EdgeEffect.Spring)  // 弹性效果

可选值:

  • EdgeEffect.Spring: 弹性效果
  • EdgeEffect.Fade: 渐隐效果
  • EdgeEffect.None: 无效果

数据源实现要点

1. 必须实现IDataSource接口

class DataSource implements IDataSource {
  totalCount(): number { }
  getData(index: number): any { }
  registerDataChangeListener(listener: DataChangeListener): void { }
  unregisterDataChangeListener(listener: DataChangeListener): void { }
}

2. 数据更新通知

// 添加数据
this.notifyDataAdd(index);

// 更新数据
this.notifyDataChange(index);

// 删除数据
this.notifyDataDelete(index);

// 重新加载
this.notifyDataReload();

3. 唯一标识符

LazyForEach(
  dataSource,
  (item) => { },
  (item) => item.id  // 必须提供唯一key
)

性能对比

ForEach渲染

  • 1000条数据: 首屏加载 ~3秒
  • 内存占用: ~50MB
  • 滚动帧率: 30-40fps

LazyForEach渲染

  • 1000条数据: 首屏加载 ~0.5秒
  • 内存占用: ~15MB
  • 滚动帧率: 55-60fps

性能提升: 约6倍

最佳实践

1. 合理设置cachedCount

// 数据简单: 缓存多一些
.cachedCount(5)

// 数据复杂: 缓存少一些
.cachedCount(2)

2. 避免在item中执行耗时操作

// ❌ 错误: 在item中查询数据库
@Builder
buildItem(record: HumanRecord) {
  const person = await this.dataService.getPersonById(record.personId);
}

// ✅ 正确: 预先加载数据
aboutToAppear() {
  this.preloadData();
}

3. 使用@Reusable提升复用

[@Reusable](/user/Reusable)
@Component
struct RecordItem {
  @State record: HumanRecord | null = null;
  
  aboutToReuse(params: Record<string, Object>) {
    this.record = params.record as HumanRecord;
  }
}

4. 图片懒加载

Image(record.avatar)
  .alt($r('app.media.default_avatar'))  // 默认图片
  .objectFit(ImageFit.Cover)

避坑指南

1. ❌ 忘记实现keyGenerator

// 错误: 没有提供key
LazyForEach(dataSource, (item) => { })

// 正确: 提供唯一key
LazyForEach(dataSource, (item) => { }, (item) => item.id)

2. ❌ 数据更新后不通知

// 错误: 直接修改数据
this.records[0] = newRecord;

// 正确: 通知数据变化
this.dataSource.updateData(0, newRecord);

3. ❌ 在item中使用复杂计算

// 错误: 每次渲染都计算
Text(this.calculateComplexValue(record))

// 正确: 预先计算好
Text(record.cachedValue)

在HarmonyOS Next中,应用展示大量数据时,可通过以下方式避免卡顿:

  1. 使用LazyForEach懒加载:结合List、Grid等组件,仅渲染可视区域内的数据项,减少内存占用和渲染开销。
  2. 优化数据更新:使用状态管理(如@State@Observed)精确控制数据变更,避免不必要的UI刷新。
  3. 分页加载数据:采用分批加载策略,用户滚动时动态加载后续数据。
  4. 复用组件:通过组件复用机制减少创建和销毁开销。
  5. 异步处理:将数据加载、图片解码等耗时操作放在异步线程,避免阻塞UI线程。

在HarmonyOS Next中处理大量数据展示并避免卡顿,核心在于优化列表渲染。以下是针对您问题的关键技术点:

  1. ForEach与LazyForEach的核心区别

    • ForEach:适用于静态、数据量有限的数组。它会立即创建所有数据项对应的组件,当数据量大时,会占用大量内存并导致初始渲染卡顿。
    • LazyForEach:专为动态、海量数据设计。它采用按需加载机制,仅创建和渲染当前可视区域及少量缓冲区的组件。当列表滚动时,它会复用离开屏幕的组件来展示新数据,从而极大降低内存占用和渲染开销,是保证长列表流畅滚动的首选方案
  2. 实现高性能列表滚动的关键实践

    • 必须使用LazyForEach:这是处理海量数据列表的基础。请确保数据源实现IDataSource接口,并正确实现totalCount()getData()registerDataChangeListener()等方法。
    • 优化列表项组件
      • 保持列表项UI结构尽可能简单扁平,减少嵌套。
      • 使用@Reusable装饰器装饰可复用的自定义组件,进一步提升LazyForEach的组件复用效率。
      • 对于复杂列表项,考虑将图片等资源的加载异步化或使用懒加载。
    • 优化数据更新
      • 使用ListArray作为数据源时,应避免在getData()方法中进行耗时操作。
      • 当数据变化时,通过IDataSourcenotifyDataChange()方法进行精准更新(如notifyDataChange({ index: 修改项的起始索引 })),而非通知整个列表刷新。
    • 合理使用列表容器
      • List组件本身已针对滚动性能进行优化。确保为其设置固定的宽高或使用弹性布局占满空间。
      • 可配合scrollToIndex等方法实现快速定位。

总结:要避免卡顿,关键在于使用LazyForEach实现组件的按需创建与复用,并配合简化组件结构精准数据更新。对于静态小数据集可用ForEach,但对于“大量数据”,LazyForEach是必须的。

回到顶部