Scroll + List 虚拟列表 — 长列表性能优化全攻略

在这里插入图片描述

一、引言

在记账应用中,账单列表是最核心的数据展示场景,可能包含数百甚至上千条交易记录。如果一次性渲染所有列表项,将导致严重的内存占用和帧率下降。鸿蒙系统提供了 Scroll + List 组合方案,结合 ForEach / LazyForEach 实现虚拟列表渲染,只渲染可视区域内的列表项,大幅提升长列表性能。MoneyTrack 在资产详情页和首页账单流中广泛运用了这套方案。

二、核心知识点

2.1 Scroll 容器

Scroll 是可滚动容器组件,常用属性如下:

  • scrollable:控制滚动方向,取值 ScrollDirection.Vertical(垂直)或 ScrollDirection.Horizontal(水平),默认垂直
  • scrollBar:滚动条显示模式,可选 BarState.Auto(自动显隐)、BarState.Always(常驻)、BarState.Off(隐藏)
  • edgeEffect:边缘效果,EdgeEffect.Spring(弹性回弹)、EdgeEffect.Fade(淡出渐变)、EdgeEffect.None(无效果)
  • enableScrollInteraction:布尔值,控制是否允许用户手势滚动,设为 false 可禁用滚动交互

Scroll 可以包裹任意内容,但不具备虚拟化能力,适合内容长度可控(如几十个组件)的场景。当内容数据量较大时,应优先使用 List。

2.2 List + ForEach / LazyForEach

List 是专门为列表场景优化的组件,支持虚拟列表机制。ForEach 与 LazyForEach 的对比如下:

对比维度 ForEach LazyForEach
渲染策略 全量参与布局计算,渲染可视项 按需创建/销毁组件实例
内存占用 中等(几十条可接受) 极低(适合几百上千条)
适用场景 数据量 ≤ 50 条 数据量 50 条以上
key 要求 必须提供稳定唯一 key 必须提供稳定唯一 key
数据变更 全量刷新 按索引增量更新

LazyForEach 需要实现 IDataSource 接口:

class TransactionDataSource implements IDataSource {
  private dataArray: TransactionItem[] = [];

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

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

  registerDataChangeListener(listener: DataChangeListener): void {}
  unregisterDataChangeListener(listener: DataChangeListener): void {}
}

// 使用 LazyForEach
List({ space: 8, scroller: this.listScroller }) {
  LazyForEach(this.dataSource, (item: TransactionItem) => {
    ListItem() {
      TransactionCard({ transaction: item })
    }
  }, (item: TransactionItem) => item.id)
}

2.3 List 辅助属性

除了基础滚动,List 还提供以下实用属性:

  • stickyHeader:设置分组粘性标题,取值 StickyStyle.Header(吸顶)或 StickyStyle.None(不吸顶)。在按日期分组的账单列表中非常实用,用户滚动时始终能看到当前日期分组
  • divider:列表项分割线,配置颜色和左右边距,例如 .divider({ strokeWidth: '1px', color: '#E8E8E8', startMargin: 16, endMargin: 16 })
  • cachedCount:设置列表项缓存数量,在可视区域外额外预创建 N 个组件,提升快速滚动时的体验,建议设为 1~3
List({ space: 8, scroller: this.listScroller }) {
  ForEach(this.viewModel.transactions, (tx: TransactionItem) => {
    ListItem() {
      TransactionCard({ transaction: tx })
    }
  }, (tx: TransactionItem) => tx.id)
}
.edgeEffect(EdgeEffect.Spring)
.divider({ strokeWidth: '1px', color: '#E8E8E8' })
.stickyHeader(StickyStyle.Header)
.cachedCount(2)

三、虚拟列表渲染原理

数据源 DataSource

虚拟列表引擎

判断是否在可视区域

创建/复用 ListItem 组件

不创建 / 销毁回收

渲染到屏幕

用户滚动触发新位置

虚拟列表的核心思想是"只渲染用户看得到的部分"。当用户快速滚动时,滑出屏幕的列表项被回收,新进入区域的列表项被创建或复用,从而将 DOM 节点数量控制在可视高度对应的范围内。

四、性能监控:验证虚拟列表是否生效

使用 DevEco Studio 的 Profiler 工具可以监控列表性能:

  1. 查看组件树节点数:打开 DevTools > Elements 面板,滚动列表时观察节点数是否保持稳定。若节点数随滚动持续增长,说明虚拟化未生效
  2. 分析内存占用:Profiler > Memory,观察滚动过程中内存是否平稳。若内存持续上涨,可能存在列表项未被正确回收
  3. 帧率检测:Profiler > Frame,确保滚动帧率稳定在 60fps。若出现掉帧,可适当增大 cachedCount 减少快速滚动时的白屏

五、最佳实践

  1. ListItem 组件化:将列表项封装为独立组件(如 TransactionCard),避免列表项内部状态变化触发整个列表刷新
  2. 稳定 key 值:务必为 ForEach/LazyForEach 提供唯一且稳定的 key 函数,避免使用索引作为 key,否则会导致渲染异常
  3. 合理配置缓存大小cachedCount 并非越大越好,过大会增加内存占用;建议设为 1~3,兼顾快速滚动体验与内存效率
  4. 选择正确的渲染方式:数据量超过 50 条时优先使用 LazyForEach;对于频繁增删操作的列表,LazyForEach 的增量更新优势更明显

六、总结

Scroll + List + ForEach/LazyForEach 构成了鸿蒙长列表性能优化的核心方案。Scroll 作为通用滚动容器,适合内容可控的场景;List 结合虚拟列表机制,专门针对大量列表数据优化。ForEach 适用于中等数据量,LazyForEach 更适合大数据量场景,其 IDataSource 接口提供了灵活的数据管理能力。在实际开发中,配合 stickyHeader、divider 等辅助属性和 cachedCount 缓存优化,可以构建出流畅、高性能的长列表页面。

七、参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐