HarmonyOS 列表卡顿优化实战:LazyForEach、复用、图片与掉帧

长列表卡顿是最容易被用户感知的问题:滑动掉帧、图片闪烁、加载更多时停顿、返回后位置丢失。ArkUI 列表优化不能只靠“少写一点 UI”,而要从数据源、组件复用、图片加载、帧率监控四层入手。本文以商品列表/文章列表场景讲清楚排查路径。

请添加图片描述

1. 列表卡顿先看数据稳定性

列表项必须有稳定 key。没有稳定 key,滑动时组件复用会混乱,图片和状态也容易错位。

请添加图片描述

问题 原因 优化方向
滑动掉帧 item 构建过重 拆轻组件
图片闪烁 图片尺寸和加载无序 缓存和占位
状态错位 key 不稳定 使用业务 ID
加载停顿 一次性追加过多 分页和批量

2. ArkUI 列表资料和目录

官方资料可参考 LazyForEach列表布局性能优化

entry/src/main/ets/common/list/
  ListDataSource.ets
  ItemKeyPolicy.ets
  ImageLoadPolicy.ets
  FrameMonitor.ets
  ListPerfReport.ets

3. ListDataSource 管理数据变化

export interface FeedItem {
  id: string
  title: string
  coverUrl: string
  updatedAt: number
}

export class ListDataSource {
  private items: FeedItem[] = []

  append(next: FeedItem[]): void {
    const exists = new Set(this.items.map(item => item.id))
    this.items = [...this.items, ...next.filter(item => !exists.has(item.id))]
  }

  list(): FeedItem[] {
    return [...this.items]
  }
}

数据源先去重,再追加,避免重复 item 造成布局抖动。

4. ItemKeyPolicy 提供稳定 key

export class ItemKeyPolicy {
  key(item: FeedItem): string {
    if (!item.id) throw new Error('列表项缺少业务 ID')
    return item.id
  }
}

不要用数组下标做 key。分页、插入、删除都会让下标变化。

5. 图片加载要限尺寸

export interface ImageRequest {
  url: string
  width: number
  height: number
}

export class ImageLoadPolicy {
  build(item: FeedItem): ImageRequest {
    return { url: item.coverUrl, width: 240, height: 160 }
  }
}

列表图片不应该加载原图。先确定展示尺寸,再请求合适尺寸的资源。

6. FrameMonitor 记录掉帧

请添加图片描述

export interface FrameSample {
  at: number
  fps: number
  scene: 'first_load' | 'scroll' | 'load_more'
}

export class FrameMonitor {
  private samples: FrameSample[] = []

  record(sample: FrameSample): void {
    this.samples.push(sample)
  }

  dropped(): FrameSample[] {
    return this.samples.filter(item => item.fps < 50)
  }
}

掉帧要和场景绑定。首次加载卡和滑动卡,优化方向不同。

7. 列表项组件要轻

export interface FeedItemViewState {
  title: string
  cover: ImageRequest
  timeText: string
}

export function buildFeedItemView(item: FeedItem): FeedItemViewState {
  return {
    title: item.title,
    cover: new ImageLoadPolicy().build(item),
    timeText: new Date(item.updatedAt).toLocaleDateString()
  }
}

复杂格式化不要放在组件反复构建路径里,可以提前在 view state 中处理。

8. 列表验收动作

场景 操作 预期结果
首次加载 加载 20 条 首屏无明显停顿
快速滑动 连续上下滑 FPS 基本稳定
加载更多 追加下一页 不重复、不跳动
图片弱网 限速网络 有占位,不闪烁
删除插入 中间插入数据 状态不串行
export function assertFeedItem(item: FeedItem): void {
  if (!item.id || !item.title) throw new Error('列表项缺少关键字段')
  if (!item.coverUrl.startsWith('https://')) throw new Error('封面图必须使用 HTTPS')
}

9. 列表卡顿排查表

现象 优先查看 处理建议
快速滑动掉帧 item 复杂度 拆轻组件
图片闪烁 图片缓存和尺寸 使用占位和缩略图
状态错位 key 使用业务 ID
加载更多卡 追加批次 分页追加
回到顶部 数据源重建 保持数据源实例

列表问题要把“数据变化”和“滑动帧率”放在一起看。只看 FPS 不知道是哪一批数据触发卡顿,只看数据日志又看不到用户体感。

export interface ListPerfRecord {
  scene: 'first_load' | 'scroll' | 'append_page'
  itemCount: number
  droppedFrameCount: number
  imageRequestCount: number
  at: number
}

export class ListPerfReport {
  private readonly records: ListPerfRecord[] = []

  append(record: ListPerfRecord): void {
    this.records.push(record)
  }

  risky(): ListPerfRecord[] {
    return this.records.filter(item => item.droppedFrameCount > 3 || item.imageRequestCount > item.itemCount)
  }
}

如果 imageRequestCount 明显大于可见 item 数,通常说明图片请求没有跟随可见区域收敛;如果 droppedFrameCount 集中在追加分页,优先检查一次性追加数量和 item 构建复杂度。

列表优化复测建议至少覆盖三种场景:

场景 操作 观察指标
首屏 首次进入列表 首屏构建耗时、图片请求数
快滑 连续上下滑动 FPS、掉帧次数
追加 加载下一页 是否跳动、是否重复
返回 详情页返回列表 滚动位置和数据源是否保留

列表优化还要避免两个反例。第一,把所有 item 都拆成很小的组件,但每个组件都做复杂计算,结果构建次数更多;第二,给每张图都加高清圆角、阴影和原图加载,视觉更精致但滑动更重。优化时要把视觉成本、图片成本、数据更新成本放在同一个列表场景里测。

反例 影响 更稳做法
item 内同步格式化时间 每次构建都计算 进入 view state 前处理
原图直接进列表 解码和内存压力大 使用缩略图
index 作为 key 插入删除后状态错位 使用业务 ID
一次追加 100 条 主线程短时压力高 分页小批量追加

如果列表里存在倒计时、播放状态、选中态这类动态字段,建议把动态状态和静态内容拆开。静态内容走列表数据源,动态状态用单独状态表维护,避免每秒刷新整条列表。

专项验收时可以准备 500 条本地假数据和 3 档图片尺寸:小图、普通图、超大图。先用本地数据排除网络影响,再逐步打开图片加载、分页追加和状态刷新。这样能判断卡顿来自 UI 构建、图片解码还是数据更新,而不是把所有问题混在一次测试里。

专项项 开关 目的
纯文本列表 关闭图片 观察 item 构建成本
缩略图列表 开启小图 观察正常图片成本
原图列表 开启大图 验证图片边界保护
动态状态 开启倒计时 验证刷新范围

如果专项测试中纯文本也掉帧,优先看 item 层级和同步计算;如果只有开图后掉帧,优先查图片尺寸和并发;如果只有追加时掉帧,优先查分页批次和数据去重。

最终交付时,建议把优化前后的录屏、FPS 采样和数据量一起保存。只有代码改动没有复测证据,后续别人很难判断这次优化解决的是哪类卡顿。

列表掉帧复现场景:给读者一组可执行核验

列表优化要验证快速滑动、图片加载、复用和空状态。只看静态页面,无法发现滑动时对象创建和图片解码问题。

核验维度 读者需要准备的证据
输入 页面入口、用户动作、关键参数
过程 日志、状态变化、异常分支
输出 UI 表现、回调结果、持久化结果
回归 同场景重复执行后的结果
interface ListReplayCase {
  listName: any
  itemCount: any
  droppedFrames: any
  imageCacheHit: any
}

const replay89: ListReplayCase = {
  listName: 'sample',
  itemCount: 'sample',
  droppedFrames: 'sample',
  imageCacheHit: 'sample',
}

function assertReplay89(item: ListReplayCase): void {
  if (item.itemCount > 100 && item.droppedFrames > 5 && !item.imageCacheHit) throw new Error('长列表掉帧且图片缓存未命中')
}

这组核验把长列表、掉帧和图片缓存命中关联起来,适合用于滑动性能回归。

列表滑动回放表:把文章方法变成可复现动作

列表性能要用真实密度验证。建议准备 20 条、200 条、2000 条数据,分别包含纯文本、图片、混合卡片,观察复用和图片缓存是否生效。

回放动作 核验方式
短列表 准备输入、执行操作、记录结果、给出结论
中列表 准备输入、执行操作、记录结果、给出结论
长列表 准备输入、执行操作、记录结果、给出结论
图片混合列表 准备输入、执行操作、记录结果、给出结论

列表卡顿要用数据密度压测。读者可以准备纯文本列表、图片列表、混合卡片列表,并分别使用几十条、几百条、几千条数据滑动。观察掉帧时要同时记录图片缓存命中、组件复用、分页加载和状态刷新次数。只有这些证据齐全,才能判断卡顿来自渲染、图片还是数据流。

列表性能的落地边界:不要把边界留给读者猜

列表卡顿通常来自多个因素叠加:组件创建过多、图片解码过重、状态刷新过频、分页策略不合理。读者落地时不要只改一个参数,而要分别记录渲染、图片、数据和交互四条线的证据。

落地项 处理要求
渲染看组件复用 需要有明确输入、处理边界和失败兜底
图片看缓存命中 需要有明确输入、处理边界和失败兜底
数据看分页大小 需要有明确输入、处理边界和失败兜底
交互看状态刷新 需要有明确输入、处理边界和失败兜底

这类边界写清楚后,读者不需要猜哪些逻辑属于页面、哪些属于服务、哪些属于发布前验收。文章的价值也会从“讲了一个功能”变成“给了一套可迁移的工程判断”。

列表联调步骤:按真实路径走一遍

列表联调建议准备三套数据。第一套 20 条纯文本,用来验证基础布局;第二套 200 条图文混合,用来验证图片缓存和复用;第三套 2000 条分页数据,用来验证加载、滚动和状态刷新。三套数据的掉帧、内存和图片命中结果都要记录,才能定位卡顿来源。

这一步的意义是让读者拿到文章后可以直接复现,而不是只理解概念。技术文章如果能把“输入、动作、日志、结果、失败兜底”写完整,读者照着做时出错概率会低很多。

列表验收补充:补上容易漏掉的边界

补充一个真实列表场景:用户快速滑动到第 300 条时,图片仍在加载,随后立即点击收藏。验收时要确认收藏状态只刷新当前 item,图片加载完成后不覆盖收藏状态。这个场景能同时验证复用、异步图片和局部状态。

列表联调还要观察状态刷新。很多卡顿不是 item 太复杂,而是筛选、点赞、收藏、分页回调触发了整页刷新。读者可以在滑动中连续点赞和取消收藏,记录刷新范围是否只影响当前 item。如果整页重建,就需要回到状态拆分和局部刷新设计。

这类补充不是为了增加篇幅,而是为了让读者在真实项目里少踩坑:正常路径一般最容易跑通,异常路径、退出路径和恢复路径才是质量差距所在。

列表收尾核验:补齐最后一个真实场景

如果列表里有曝光埋点,还要确认复用不会造成重复曝光。滑动到同一张卡片、返回再进入、切换筛选后再次出现,三种情况下的曝光规则应不同。性能优化不能牺牲数据准确性,否则列表不卡了,后续运营分析却会失真。

这个核验点建议和性能日志一起保存。

这个场景建议打开列表刷新日志,记录筛选条件、分页游标和当前可见 item 范围。切换筛选时如果旧分页继续回调,应丢弃旧游标结果;否则新筛选列表会被旧数据污染。

最后再补一个交互场景:用户滑动过程中切换筛选条件。验收时要确认旧列表请求被丢弃,新列表状态重新计算,滚动位置和空状态不会互相污染。这个场景能发现分页状态和筛选状态没有隔离的问题。

10. 小结:列表性能看复用和图片

列表优化要先保证数据稳定,再控制 item 构建复杂度和图片加载成本。LazyForEach 不是万能药,稳定 key、分页数据、缩略图和帧率采样一起做,列表才能真正流畅。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐