鸿蒙 ArkUI 长列表性能优化:从掉帧到稳定 60 帧的实践清单

举报
yd_227460417 发表于 2026/11/07 13:40:38 2026/11/07
【摘要】 长列表是鸿蒙应用里最容易出现掉帧的场景之一。本文从数据源、组件复用、状态粒度、布局和图片几个维度,整理一套可以直接落地的 ArkUI 长列表优化清单,适合已经能写出可运行页面、但列表滚动仍不够顺滑的开发者。 一、先确认问题出在哪列表卡顿通常不是单一原因,建议先做一次简单定位:首屏渲染慢:进入页面后长时间白屏或骨架屏停留过久;滚动掉帧:滑动时明显顿挫,帧率波动大;操作延迟:点击、切换状态后响应...

长列表是鸿蒙应用里最容易出现掉帧的场景之一。本文从数据源、组件复用、状态粒度、布局和图片几个维度,整理一套可以直接落地的 ArkUI 长列表优化清单,适合已经能写出可运行页面、但列表滚动仍不够顺滑的开发者。

一、先确认问题出在哪

列表卡顿通常不是单一原因,建议先做一次简单定位:

  1. 首屏渲染慢:进入页面后长时间白屏或骨架屏停留过久;
  2. 滚动掉帧:滑动时明显顿挫,帧率波动大;
  3. 操作延迟:点击、切换状态后响应慢;
  4. 内存上涨:滑动越久内存越高,甚至触发回收。

对应到 ArkUI,常见根因是:一次性创建全部列表项、状态刷新范围过大、布局层级过深、图片解码占用主线程。先分清属于哪一类,再针对性处理。

二、用 LazyForEach 替代一次性 ForEach

ForEach 会在构建时创建全部子组件,数据量一大,首屏就会被拖慢。长列表应使用 LazyForEach,按需创建可见区域附近的节点。

LazyForEach 需要一个数据源实现 IDataSource:

class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private data: FeedItemData[] = [];

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

  getData(index: number): FeedItemData {
    return this.data[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);
    }
  }

  push(item: FeedItemData): void {
    this.data.push(item);
    this.listeners.forEach(l => l.onDataAdd(this.data.length - 1));
  }

  reset(items: FeedItemData[]): void {
    this.data = items;
    this.listeners.forEach(l => l.onDataReloaded());
  }
}

页面里这样使用:

LazyForEach(this.source, (item: FeedItemData) => {
  FeedItem({ item: item })
}, (item: FeedItemData) => item.id)

第三个参数是键生成函数,必须保证唯一且稳定。用数组下标做键,在插入或删除时会导致复用错位。

三、开启组件复用

LazyForEach 解决的是“按需创建”,组件复用解决的是“重复创建”。使用 @Reusable 装饰列表项组件,配合 aboutToReuse 更新数据:

@Reusable
@Component
struct FeedItem {
  @State item: FeedItemData = { id: '', title: '', cover: '', summary: '' };

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

  build() {
    Row() {
      Image(this.item.cover)
        .width(72)
        .height(72)
        .objectFit(ImageFit.Cover)
      Column() {
        Text(this.item.title)
          .fontSize(16)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        Text(this.item.summary)
          .fontSize(13)
          .fontColor('#666666')
          .maxLines(1)
      }
      .layoutWeight(1)
      .margin({ left: 12 })
    }
    .width('100%')
    .padding(12)
  }
}

注意两点:

  • 复用组件内部不要保存只属于某一条数据的临时状态;
  • aboutToReuse 里只做数据赋值,不要做网络请求或复杂计算。

四、缩小状态刷新范围

列表项里用了 @State,任何一次赋值都会触发该组件重新渲染。常见误区是把整个列表数据放在页面级 @State 里,然后每次更新都整体替换,导致整页刷新。

更稳妥的做法:

  1. 列表数据源交给 LazyForEach 的 IDataSource 管理;
  2. 单个列表项内部状态用 @State,只影响自身;
  3. 跨组件共享且需要观察的对象用 @Observed 与 @ObjectLink,避免深层对象整体刷新;
  4. 只用于展示、不会变化的字段,不要加状态装饰器。

五、控制布局层级

ArkUI 的布局计算与层级深度相关。列表项里嵌套过多 Column、Row、Stack,单个节点开销不大,但乘以可见数量后就会明显。

几个实用原则:

  • 能用 Row、Column 直接排布的,不要额外包一层容器;
  • 固定宽高的图片和图标显式设置尺寸,减少测量;
  • 列表项高度尽量一致,避免频繁重新测量;
  • 避免在 build 中做字符串拼接、排序、过滤等计算,提前在数据层处理好。

六、图片是最大的隐形开销

图片解码和内存占用往往是掉帧的主要来源。建议:

  • 列表缩略图使用合适尺寸,不要直接加载原图;
  • Image 显式设置宽高,避免布局阶段反复调整;
  • 快速滑动场景谨慎使用同步加载,优先让图片异步加载;
  • 长列表项离开可视区域后及时释放大图引用;
  • 头像、图标这类小图尽量复用同一资源对象。

七、动效要克制

animateTo 和属性动画能提升体验,但在滚动过程中对大量节点同时做动画,会明显增加渲染压力。

建议只在交互反馈上使用短时动画,列表滚动期间不加额外动效;删除、插入等操作可以使用局部动画,不要对整列表做统一过渡。

八、验收指标

优化是否有效,不要只靠“感觉顺了”,建议记录三个指标:

  1. 首屏可见时间:从进入页面到首屏内容出现;
  2. 滚动帧率:连续滑动 10 秒,观察是否稳定在接近 60 帧;
  3. 内存曲线:反复上下滑动后,内存是否持续上涨。

可以用 DevEco Studio 自带的 ArkUI Inspector 查看组件树和布局层级,用性能分析器观察帧率与耗时。重点看两件事:可见区域外的节点是否仍在创建,以及单帧耗时是否集中在布局或图片解码。

九、小结

长列表优化的顺序建议是:先换 LazyForEach,再开组件复用,然后缩小状态范围,最后处理布局和图片。每一步都能独立验证效果,不要一次性全改,否则很难判断是哪一项起了作用。

真正稳定的列表,通常不是靠某一个“高级 API”,而是靠数据源、复用、状态和资源四条线同时收敛。先把这四件事做扎实,再考虑更复杂的预加载和缓存策略。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。