积分中心
ArkUI 长列表卡顿排查与优化:从 ForEach 到 LazyForEach
头像 ccai 2026-09-10 11:34:29    发布
6555 浏览 37 点赞 0 收藏

摘要

列表数据从几十条增长到数百条后,页面可能出现首屏变慢、快速滑动掉帧和局部修改导致全量刷新的问题。本文以车辆使用记录列表为例,说明如何区分数据处理、节点创建、状态更新和图片解码带来的成本,并介绍 LazyForEach、稳定 key、展示模型预计算和分批加载等方法。文中不提供脱离设备的通用性能承诺,测试数据应以实际项目记录为准。

一、先记录基线,而不是直接改代码

最初的列表写法通常很直接:

@State private records: VehicleRecord[] = []

build() {
  Scroll() {
    Column() {
      ForEach(this.records, (item: VehicleRecord) => {
        RecordRow({ record: item })
      }, (item: VehicleRecord) => item.id)
    }
  }
}

当数据只有几十条时,这种写法通常足够。但数据量变大后,可能出现:

  • 首次进入页面等待时间增加;
  • 快速上下滑动时掉帧;
  • 修改一条记录却触发大量节点重新计算;
  • 列表图片解码占用较多内存。

定位时应固定以下条件:

  • 数据量和字段数量;
  • Debug 或 Release 构建;
  • 设备型号和系统版本;
  • 图片尺寸和网络状态;
  • 首次进入还是热启动;
  • 测量次数和统计方式。

二、长列表使用惰性数据源

LazyForEach 应放在 List、Grid 或其他支持惰性加载的容器内。下面只展示数据源核心方法,监听器的完整回调名称和参数请以当前 SDK 的 DataChangeListener 定义为准。

class RecordDataSource implements IDataSource {
  private records: RecordViewData[] = []
  private listeners: DataChangeListener[] = []

  totalCount(): number {
    return this.records.length
  }

  getData(index: number): RecordViewData {
    return this.records[index]
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    this.listeners.push(listener)
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const index = this.listeners.indexOf(listener)
    if (index >= 0) {
      this.listeners.splice(index, 1)
    }
  }

  update(index: number, item: RecordViewData): void {
    if (index < 0 || index >= this.records.length) {
      return
    }

    this.records[index] = item
    this.listeners.forEach(listener => {
      listener.onDataChange(index)
    })
  }

  reload(items: RecordViewData[]): void {
    this.records = items

    // 调用当前 SDK 对应的“全量刷新”回调
    // 不同版本的回调名称请以类型定义为准
  }
}

页面使用方式:

private dataSource: RecordDataSource = new RecordDataSource()

build() {
  List() {
    LazyForEach(this.dataSource, (item: RecordViewData) => {
      ListItem() {
        RecordRow({ record: item })
      }
    }, (item: RecordViewData) => item.id)
  }
  .cachedCount(4)
  .width('100%')
  .layoutWeight(1)
}

这里有三个关键点:

  1. key 必须稳定且唯一,建议使用业务 ID;
  2. 修改数据后必须通知数据源监听器;
  3. cachedCount 不宜盲目调大,缓存越多,内存占用也可能越高。

三、避免在 build 中反复做复杂计算

不要在列表项构建阶段反复进行日期格式化、状态转换或复杂字符串拼接:

Text(formatDate(item.createTime))
Text(calculateStatusText(item.status))

可以在数据进入 UI 前转换为展示模型:

interface RecordViewData {
  id: string
  vehicleName: string
  displayDate: string
  displayMileage: string
  statusText: string
}

function toViewData(item: VehicleRecord): RecordViewData {
  return {
    id: item.id,
    vehicleName: item.vehicleName,
    displayDate: formatDate(item.createTime),
    displayMileage: `${item.mileage} km`,
    statusText: getStatusText(item.status)
  }
}

这样做不仅有助于减少 UI 阶段的计算,也可以让格式化逻辑被单独测试。

四、图片要按场景加载

列表只需要缩略图,详情页再加载原图。图片地址为空时,提供默认资源:

Image(this.record.imageUrl.length > 0
  ? this.record.imageUrl
  : $r('app.media.default_vehicle'))
  .width(72)
  .height(52)
  .objectFit(ImageFit.Cover)
  .borderRadius(6)

还应注意:

  • 不要在滚动过程中重复进行复杂图片处理;
  • 列表项的图片区域尽量使用固定比例;
  • 网络图片要有失败占位;
  • 进入详情页后再加载大图;
  • 图片缓存策略要结合设备内存验证。

五、其他常见优化

如果数据解析本身很重,可以把纯计算任务放到任务池或其他后台执行能力中,但后台任务不能直接访问 UI 状态。搜索框输入也不宜每个字符都触发完整查询,可以使用短时间防抖。

同时检查以下问题:

  • Scroll 内部是否又嵌套了可滚动列表;
  • 是否在每次刷新时重新创建相同的数据源;
  • 是否直接改变数组却没有通知 UI;
  • 是否使用数组下标作为 key;
  • 是否把原图直接放进长列表;
  • 是否在 build 中执行网络请求或大规模 JSON 解析。

六、验证表应填真实数据

发布时建议使用如下表格,并将【待填】替换为实际测量值:


指标优化前优化后测量方法
首次有效内容出现时间【待填】【待填】Release 构建,重复 3~5 次
快速滑动帧率或帧时间【待填】【待填】同一设备、同一数据集
峰值内存【待填】【待填】同一图片和缓存条件
单条记录修改影响范围【待填】【待填】日志或 UI 检查

如果暂时没有性能工具或稳定数据,就写清楚“本文给出测试方案,数据待补”,不要用估算数字替代实测。

七、总结

列表优化不是简单地把 ForEach 换成 LazyForEach。真正需要同时考虑的是:

  • 数据量是否适合一次性创建;
  • 数据源是否正确通知变化;
  • key 是否稳定;
  • UI 构建阶段是否做了过多计算;
  • 图片是否使用了合适的尺寸;
  • 测试是否在相同条件下进行。

几十条以内的简单列表可以优先保持代码直观;当数据量、列表复杂度和局部更新频率上升后,再引入惰性数据源和更细粒度的更新机制。


©本站发布的所有内容,包括但不限于文字、图片、音频、视频、图表、标志、标识、广告、商标、商号、域名、软件、程序等,除特别标明外,均来源于网络或用户投稿,版权归原作者或原出处所有。我们致力于保护原作者版权,若涉及版权问题,请及时联系我们进行处理。
分类
HarmonyOS
地址:北京市朝阳区北三环东路三元桥曙光西里甲1号第三置业A座1508室 电话:13391790444或(010)62178877
版权所有:电脑商情信息服务集团 北京赢邦策略咨询有限责任公司
声明:本媒体部分图片、文章来源于网络,版权归原作者所有,我司致力于保护作者版权,如有侵权,请与我司联系删除

京ICP备:2022009079号-2

京公网安备:11010502051901号

ICP证:京B2-20230255