文档开发者中心
我的
|

ArkUI (方舟UI框架)

ArkUI的List组件在大量数据下滑动卡顿,如何优化?

在开发鸿蒙应用时,使用ArkUI的List组件展示数千条数据,滑动时明显卡顿。已尝试使用LazyForEach进行懒加载,但滚动时仍掉帧。请问还有哪些优化方案?是否需要配合缓存、分片加载或调整item复用策略?

点赞
收藏
2
回复
6
分享
举报
浏览26 发布于1小时前广西
全部评论
最多点赞
最新发布
最早发布
你已经用了LazyForEach,方向是对的,接下来按优先级依次做:确认LazyForEach真正生效—>调大cachedCount—>加@Reusable组件复用—>固定item高度—>优化图片解码,这几步组合下来,千级列表也能稳定60fps。
6楼回复于10分钟前 来自浙江

你已经用了LazyForEach,方向是对的,但光靠懒加载还不够。官方的长列表加载丢帧优化总结了5个优化手段,按优先级给你捋一下:

1. 加 cachedCount 预加载

LazyForEach只加载可视区组件,快速滑动时新item来不及创建就会出现白块。cachedCount设置屏幕外预加载数量,让即将进入视口的item提前准备好:

收起
自动换行
深色代码主题
复制
List() {
  LazyForEach(this.dataSource, (item: ItemData) => {
    ListItem() {
      MyItemComponent({ data: item })
    }
  }, (item: ItemData) => item.id.toString())
}.cachedCount(5)

cachedCount只跟LazyForEach配合生效,值别设太大,3-5个够了,否则预加载本身又成了负担。

2. 组件复用 @Reusable

这是解决滚动掉帧的关键。ListItem滑出屏幕后被销毁,滑回来又要重建,频繁创建/销毁是掉帧的主因。给ListItem内的自定义组件加@Reusable,框架会回收组件实例到缓存池复用,而不是每次重新创建:

收起
自动换行
深色代码主题
复制
@Reusable
@Component
struct MyItemComponent {
  @State data: ItemData = new ItemData()

  aboutToReuse(params: Record<string, Object>) {
    this.data = params.data as ItemData
  }

  build() {
    Row() {
      Text(this.data.title)
    }
  }
}

注意aboutToReuse里要重置状态,否则复用时可能显示上一次的残留数据。如果你的项目用的是V2状态管理(@ComponentV2),对应装饰器是@ReusableV2,在aboutToReuse回调中处理状态重置。

3. keyGenerator 用业务ID别用index

LazyForEach的第三个参数keyGenerator必须返回唯一且稳定的键值,用index当key会导致数据更新时组件错乱或重建,直接拖慢性能。务必用业务ID:

收起
自动换行
深色代码主题
复制
// 对
(item: ItemData) => item.id.toString()
// 错
(item: ItemData, index: number) => index.toString()

另外keyGenerator里别用JSON.stringify,序列化大对象的耗时会让性能更差。

4. 布局扁平化

ListItem内部嵌套层级越深,测量和绘制越慢。尽量控制在3层以内,减少Row/Column的无意义嵌套。如果布局复杂,考虑用RelativeContainer减少层级。

5. 动态预加载(API 12+)

如果以上都做了还是快速滑动白块,可以考虑用Repeat替代LazyForEach。Repeat是API 12引入的可复用循环渲染,内置了组件复用机制,配合cachedCount和template可以更精细地控制多类型列表项的复用,参考Repeat:可复用的循环渲染。

另外官方还提供了一个ScrollComponents库,内置了组件复用、懒加载、复用池共享等优化,适合不想手动调优的场景。

最后排查几个常见坑:List外面套了Scroll会导致LazyForEach失效(全部一次性加载);ListItem没设固定高度也会让懒加载失效;多层嵌套List要给内层List固定高度。

展开全部
5楼回复于1小时前 来自四川

List 大数据量卡顿,绝大多数情况是一次性加载全部子组件导致的。按优先级给你一套排查和优化方案:

1. 用 LazyForEach 替代 ForEach(最关键)

ForEach 会一次性创建并渲染所有列表项;LazyForEach 按需创建、视口外自动销毁,这是长列表优化的第一原则。注意 LazyForEach 必须配合 IDataSource 实现(数据变化时调用 notifyDataAdd/notifyDataChange 等),不能用普通数组直接塞进去。

收起
自动换行
深色代码主题
复制
class MyDataSource implements IDataSource {
  private list: number[] = [];
  totalCount(): number { return this.list.length; }
  getData(i: number): number { return this.list[i]; }
  registerDataChangeListener(l: DataChangeListener): void {}
  unregisterDataChangeListener(l: DataChangeListener): void {}
}

LazyForEach(this.data, (item: number) => {
  ListItem() { ItemView({ item: item }) }
}, (item: number) => item.toString())

2. 设置 cachedCount 预加载缓冲

cachedCount(5) 表示视口外预创建 5 个子组件,滑动时避免临时创建带来的掉帧。值不宜过大,否则又退化成全量创建。

3. 组件复用 @Reusable

给列表项组件加 @Reusable 装饰器,滑动时复用已销毁组件的节点,只更新数据不重建节点树,配合 aboutToReuse 回调刷新状态,对图片列表、聊天列表这类高频滑动场景提升非常明显。

4. 减少单个子项的渲染开销

• 列表项高度尽量固定或可预估(等高列表性能最好);

• 避免在 item 里做复杂嵌套布局和大量小图片,图片用合适的尺寸而不是原图缩放;

• 避免在 build 里做函数调用计算,复杂计算放 @Computed 或提前算好。

5. 状态管理粒度

@State 数组整体更新会触发大范围 diff,尽量用 @ObjectLink/@Observed 把状态下沉到单个 item 内部,滑动中只刷新可见项。

6. 进阶

超大数据量(上万条)考虑分段加载(滚动到底部再 append),配合 Scroll + List 嵌套时注意嵌套滚动配置;HarmonyOS NEXT 上也可以直接用 AiGUI 的 Repeat 能力(API 12+),它是 LazyForEach 的增强版,支持作用域更新,官方更推荐。

按这个顺序改,一般 List 卡顿都能解决。如果还有问题,可以贴一下 item 布局代码和数据量级,帮你进一步定位。

展开全部
4楼回复于1小时前 来自广东

用了LazyForEach还掉帧,大概率是没用上缓存预加载和组件复用。官方测试数据:万条数据下,仅用LazyForEach丢帧率6.6%,加上cachedCount降到3.7%,再加上@Reusable组件复用降到0%。以下逐条说。

1. 设置cachedCount预加载

LazyForEach只加载屏幕可见区域,快速滑动时新item来不及创建就白块/掉帧。cachedCount让List在空闲时提前创建屏幕外的列表项,滑过来直接渲染。

建议值设为一屏显示数量的一半左右,官方测试cachedCount=3(一屏6条)时丢帧率最低。太大反而拖慢首屏、增加内存。

收起
自动换行
深色代码主题
复制
List() {
  LazyForEach(this.dataSource, (item: ItemData) => {
    ListItem() {
      MyItemView({ data: item })
    }
    .reuseId('myItem')
  }, (item: ItemData) => item.id)
}
.cachedCount(3)

2. 用@Reusable实现组件复用

这是解决"规律性丢帧"最有效的手段。没复用时,每次新item进屏幕都要从零创建组件(BuildLazyItem约10ms),超过11.1ms就丢帧。复用后直接从缓存池取组件更新数据(约0.75ms),丢帧率直接降到0。

收起
自动换行
深色代码主题
复制
@Component
@Reusable
struct MyItemView {
  @Prop data: ItemData = new ItemData()

  aboutToReuse(params: Record<string, ESObject>): void {
    // 处理回调等非@Prop参数的重置
  }

  build() {
    // item布局,尽量扁平化
  }
}

ListItem上要加.reuseId标记复用组:

收起
自动换行
深色代码主题
复制
ListItem() {
  MyItemView({ data: item })
}
.reuseId('myItem')

混合布局中多种卡片类型时,给不同类型设不同reuseId,避免错误复用触发重建。

3. 动态预加载(Prefetcher)

如果列表含网络图片等资源,弱网+快速滑动时cachedCount不够用,会出白块。用BasicPrefetcher动态调整预取范围,把数据预取和预渲染分离,智能取消不需要的任务。

收起
自动换行
深色代码主题
复制
import { BasicPrefetcher } from '@kit.ArkUI'

// 数据源实现IDataSourcePrefetching接口的prefetch/cancel方法
private readonly prefetcher = new BasicPrefetcher(this.dataSource)

List() {
  LazyForEach(this.dataSource, (item: ItemData) => {
    ListItem() { /* ... */ }
  })
}
.cachedCount(5)
.onScrollIndex((start: number, end: number) => {
  this.prefetcher.visibleAreaChanged(start, end)
})

官方测试:动态预加载首屏545ms(接近cachedCount=5的530ms),但滑动时几乎无白块(cachedCount=40才能做到,但首屏要1.8s)。

4. 布局扁平化

ListItem嵌套层级每多一层,测量绘制都多一份开销。官方测试:5层嵌套→78MB/0%丢帧,25层嵌套→153MB/2.3%丢帧。建议控制在5-8层以内,复杂卡片用RelativeContainer替代多层Row/Column嵌套。

5. 注意LazyForEach失效的坑

几个常见导致懒加载失效的场景:

  • Scroll嵌套List且滚动方向相同,List未设主轴尺寸 → 全量加载,改用ListItemGroup
  • keyGenerator用了JSON.stringify → 性能杀手,用业务id
  • ListItem未设固定高度(嵌套List场景) → 懒加载失效

完整最小示例:

收起
自动换行
深色代码主题
复制
class ItemData {
  id: string = ''
  title: string = ''
}

class ListDataSource implements IDataSource {
  private dataArray: ItemData[] = []
  private listeners: DataChangeListener[] = []

  constructor(data: ItemData[]) { this.dataArray = data }
  totalCount(): number { return this.dataArray.length }
  getData(index: number): ItemData { return this.dataArray[index] }
  registerDataChangeListener(l: DataChangeListener): void { this.listeners.push(l) }
  unregisterDataChangeListener(l: DataChangeListener): void {
    const idx = this.listeners.indexOf(l)
    if (idx >= 0) { this.listeners.splice(idx, 1) }
  }
}

@Entry
@Component
struct LongListPage {
  private dataSource: ListDataSource = new ListDataSource([])

  aboutToAppear(): void {
    const items: ItemData[] = []
    for (let i = 0; i < 10000; i++) {
      const item = new ItemData()
      item.id = String(i)
      item.title = 'Item ' + i
      items.push(item)
    }
    this.dataSource = new ListDataSource(items)
  }

  build() {
    List({ space: 8 }) {
      LazyForEach(this.dataSource, (item: ItemData) => {
        ListItem() {
          ReusableItem({ data: item })
        }
        .reuseId('reusableItem')
      }, (item: ItemData) => item.id)
    }
    .cachedCount(3)
    .width('100%')
    .height('100%')
  }
}

@Component
@Reusable
struct ReusableItem {
  @Prop data: ItemData = new ItemData()

  aboutToReuse(params: Record<string, ESObject>): void {}

  build() {
    Row() {
      Text(this.data.title)
        .fontSize(16)
    }
    .width('100%')
    .height(60)
    .padding({ left: 16, right: 16 })
  }
}

上面代码覆盖了LazyForEach + cachedCount + @Reusable三个核心优化。动态预加载需要网络资源场景才用得上。布局扁平化视你的item复杂度而定,ArkUI Inspector可以查看嵌套层级。用DevEco Studio的Profiler抓帧率就能看到优化效果。

展开全部
3楼回复于1小时前 来自四川
AI 生成内容,仅供参考

List 组件在数千条数据下即使使用了 LazyForEach 仍掉帧,通常是因为懒加载只解决了"按需创建"的问题,而快速滑动时每个列表项仍在频繁走"创建-销毁"流程,同时缓存区、组件复用、数据更新粒度、布局层级等因素未做配套优化。完整的优化方案需要 LazyForEach + cachedCount 预加载 + @Reusable 组件复用 + 分页分片加载 + 布局精简多管齐下。

问题解释

LazyForEach 的作用是让 List 只构建可视区域内的组件,但它在组件滑出可视区域(含缓存区)后默认会销毁组件,滑入时再重新创建。当列表项结构较复杂(含图片、多层嵌套布局)时,这个反复创建和布局计算的开销就会直接体现为掉帧。因此官方推荐的组合是:

  1. 组件复用:给列表项的自定义组件加 @Reusable 装饰器,滑出屏幕的组件进入复用池而不是被销毁,滑入时通过 aboutToReuse 回调直接回填新数据,跳过整个创建流程。实测中组件创建次数会从"约等于数据条数"收敛到"一屏条数 + cachedCount"的常数级别。
  2. cachedCount 预加载:控制可视区域外上下额外预构建的节点数,避免滑动时"现创建"。默认值较小,一般建议设为单屏可见条数的 12 倍(38 之间),过大则浪费内存。
  3. 分片加载:数千条数据不建议一次性全部构造并塞入数据源,应在 onReachEnd 触底时分页追加,并通过 DataChangeListener 的细粒度通知(onDataAdd 等)告知框架,避免全量刷新。
  4. 键值稳定:LazyForEach 依赖键值判断是否刷新,key 生成函数必须返回稳定且唯一的值,否则会引发不必要的重建。

示例代码

以下是一个完整的可运行示例(适用于 API 12 及以上版本,V1 状态管理模式),包含 IDataSource 实现、@Reusable 复用组件、cachedCount 预加载和触底分页加载:

收起
自动换行
深色代码主题
复制
// 数据模型:显式建模,避免松散对象字面量
class MessageItem {
  id: number = 0
  title: string = ''
  content: string = ''

  constructor(id: number, title: string, content: string) {
    this.id = id
    this.title = title
    this.content = content
  }
}

// IDataSource 实现:LazyForEach 的数据源
class MessageDataSource implements IDataSource {
  private listeners: DataChangeListener[] = []
  private dataArray: MessageItem[] = []

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

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

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener)
    }
  }

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

  // 分页追加数据,并逐条细粒度通知框架,避免全量刷新
  appendData(items: MessageItem[]): void {
    const start: number = this.dataArray.length
    this.dataArray = this.dataArray.concat(items)
    for (let i = 0; i < items.length; i++) {
      this.listeners.forEach((listener: DataChangeListener) => {
        listener.onDataAdd(start + i)
      })
    }
  }
}

// 可复用的列表项组件:@Reusable 必须加在 LazyForEach 生成的子组件上
@Reusable
@Component
struct MessageItemView {
  @State item: MessageItem = new MessageItem(0, '', '')

  // 复用时回填新数据,同时清理上一条遗留的临时状态(如展开态、输入态)
  aboutToReuse(params: Record<string, Object>): void {
    const reused: MessageItem = params.item as MessageItem
    this.item = reused
  }

  build() {
    // 布局层级尽量扁平,文本设置 maxLines 限制高度
    Row() {
      Column() {
        Text(this.item.title)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        Text(this.item.content)
          .fontSize(14)
          .fontColor('#666666')
          .maxLines(2)
          .margin({ top: 4 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.START)
    }
    .width('100%')
    .padding(16)
  }
}

@Entry
@Component
struct LongListPage {
  private dataSource: MessageDataSource = new MessageDataSource()
  private nextStart: number = 0
  private readonly pageSize: number = 100
  @State isLoading: boolean = false

  aboutToAppear(): void {
    this.loadMore()
  }

  // 分片加载:每次只构造一页数据,实际项目中可替换为网络请求
  loadMore(): void {
    if (this.isLoading) {
      return
    }
    this.isLoading = true
    const items: MessageItem[] = []
    for (let i = this.nextStart; i < this.nextStart + this.pageSize; i++) {
      items.push(new MessageItem(i, `标题 ${i}`, `这是第 ${i} 条消息的内容摘要`))
    }
    this.nextStart += this.pageSize
    this.dataSource.appendData(items)
    this.isLoading = false
  }

  build() {
    List({ space: 8 }) {
      LazyForEach(this.dataSource, (item: MessageItem, index: number) => {
        ListItem() {
          MessageItemView({ item: item })
        }
      }, (item: MessageItem, index: number) => item.id.toString()) // 键值稳定且唯一
    }
    .cachedCount(5) // 预加载缓冲区:单屏可见数的 1~2 倍
    .width('100%')
    .height('100%')
    .onReachEnd(() => {
      this.loadMore() // 触底时追加下一页
    })
  }
}

如果项目使用状态管理 V2(@ComponentV2),官方更推荐用 Repeat 替代 LazyForEach:Repeat(this.dataArr).virtualScroll({ totalCount: this.dataArr.length }) 配合 .cachedCount(),Repeat 自带组件复用能力,配合 @ReusableV2 装饰器即可,无需手动实现 IDataSource,API 更简洁且性能更好。

补充说明

除了上述核心手段,还有几个容易导致掉帧的细节需要排查:

  1. 数据更新粒度:不要用 this.dataSource = newDataSource 这种整体替换的方式刷新数据,会导致整个列表重建、滚动位置丢失。应使用 onDataChange、onDataDelete、onDataAdd、onDataMove 等细粒度通知,只刷新变化的条目。
  2. reuseId 区分类型:如果列表中存在多种结构的列表项(如图片卡片和文字卡片),需要通过 reuseId 标记不同类型,避免不同结构的组件互相复用导致布局反复调整。
  3. 布局优化:减少列表项内部的嵌套层级;能固定高度的条目尽量固定高度,避免高度不确定导致反复测量;不要在 ListItem 内再嵌套可滚动容器;避免在 build 中做数据排序、过滤等复杂逻辑。
  4. 图片处理:列表项中的大图应使用异步解码和合适的采样尺寸,避免在滑动过程中同步解码大图阻塞主线程;可配合 PixelMap 做图片缓存。
  5. 组件冻结:配合自定义组件冻结机制,滑出缓存区的组件会被标记为 inactive,其内部的状态刷新和动画会被暂停,可进一步降低滑动时的负载。
  6. 效果验证:使用 DevEco Studio 的 Profiler 工具抓帧分析,重点观察丢帧率、组件创建次数和复用命中率。正常调优后,快速滑动时复用命中率应稳定在较高水平,来回滚动多趟组件创建次数基本不再增长。

总结来说,LazyForEach 只是长列表优化的第一步,需要同时配上 cachedCount 缓存、@Reusable 复用策略和分片加载,并保证 key 稳定、数据更新细粒度化,才能把数千条数据的滑动帧率稳定住。

展开全部
2楼回复于1小时前 来自广东
优先确认LazyForEach生效并给List定高;调大cachedCount留缓冲;item加@Reusable复用;精简item内重操作;分片只省内存不救帧率。
1楼回复于1小时前 来自江苏
写回答
新增插入模板功能
一键使用模板,快速填写内容,轻松发帖~
知道了
  • 为了保障您的信息安全,请勿上传您的敏感个人信息(如您的密码等信息)和您的敏感资产信息(如关键源代码、签名私钥、调试安装包、业务日志等信息),且您需自行承担由此产生的信息泄露等安全风险。
  • 如您发布的内容为转载内容,请注明内容来源。

我要提问题

了解社区公约,与您携手共创和谐专业的开发者社区。