HarmonyOS掌上记账APP开发实践第22篇:Scroll + List 虚拟列表 — 长列表性能优化全攻略

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 接口:

typescript 复制代码
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
typescript 复制代码
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)

三、虚拟列表渲染原理

#mermaid-svg-yN3SpDEZxAZv9TZ4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yN3SpDEZxAZv9TZ4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .error-icon{fill:#552222;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .marker.cross{stroke:#333333;}#mermaid-svg-yN3SpDEZxAZv9TZ4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yN3SpDEZxAZv9TZ4 p{margin:0;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .cluster-label text{fill:#333;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .cluster-label span{color:#333;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .cluster-label span p{background-color:transparent;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .label text,#mermaid-svg-yN3SpDEZxAZv9TZ4 span{fill:#333;color:#333;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .node rect,#mermaid-svg-yN3SpDEZxAZv9TZ4 .node circle,#mermaid-svg-yN3SpDEZxAZv9TZ4 .node ellipse,#mermaid-svg-yN3SpDEZxAZv9TZ4 .node polygon,#mermaid-svg-yN3SpDEZxAZv9TZ4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .rough-node .label text,#mermaid-svg-yN3SpDEZxAZv9TZ4 .node .label text,#mermaid-svg-yN3SpDEZxAZv9TZ4 .image-shape .label,#mermaid-svg-yN3SpDEZxAZv9TZ4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .rough-node .label,#mermaid-svg-yN3SpDEZxAZv9TZ4 .node .label,#mermaid-svg-yN3SpDEZxAZv9TZ4 .image-shape .label,#mermaid-svg-yN3SpDEZxAZv9TZ4 .icon-shape .label{text-align:center;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .node.clickable{cursor:pointer;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .arrowheadPath{fill:#333333;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yN3SpDEZxAZv9TZ4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yN3SpDEZxAZv9TZ4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yN3SpDEZxAZv9TZ4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .cluster text{fill:#333;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .cluster span{color:#333;}#mermaid-svg-yN3SpDEZxAZv9TZ4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yN3SpDEZxAZv9TZ4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .icon-shape,#mermaid-svg-yN3SpDEZxAZv9TZ4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .icon-shape p,#mermaid-svg-yN3SpDEZxAZv9TZ4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .icon-shape .label rect,#mermaid-svg-yN3SpDEZxAZv9TZ4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yN3SpDEZxAZv9TZ4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yN3SpDEZxAZv9TZ4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yN3SpDEZxAZv9TZ4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

数据源 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 缓存优化,可以构建出流畅、高性能的长列表页面。

七、参考文档

相关推荐
不羁的木木1 小时前
《HarmonyOS技术精讲-NearLink Kit(星闪服务)》第4篇:广播通信——一对多广播应用实现
华为·harmonyos
w139548564221 小时前
鸿蒙应用开发实战【91】— 完整功能串联从添加到换绑的全链路
华为·harmonyos·鸿蒙系统
2501_919749031 小时前
华为鸿蒙免费训练口才APP—小羊口才
华为·harmonyos
木木子221 小时前
# [特殊字符] 画画模拟器 — 鸿蒙ArkTS完整技术解析
华为·harmonyos
Helen_cai1 小时前
OpenHarmony 图片选择 & 相册相机工具封装 ImagePickerUtil(API23)
数码相机·华为·harmonyos
AD022710 小时前
22-平板上页面被拉扁-用断点布局和资源覆盖做一多适配
harmonyos·arkts·鸿蒙开发
绝世番茄12 小时前
鸿蒙HarmonyOS ArkTS原生拖拽排序深度解析
深度学习·华为·list·harmonyos·鸿蒙
丁小未13 小时前
Unity 极致高效的IM设计方案教程
unity·性能优化·游戏引擎·im
Georgewu14 小时前
【HarmonyOS AI 系列文章】 图像超分:用端侧 AI 实现小图传输,高清图呈现
harmonyos