在开发鸿蒙应用时,使用ArkUI的List组件展示数千条数据,滑动时明显卡顿。已尝试使用LazyForEach进行懒加载,但滚动时仍掉帧。请问还有哪些优化方案?是否需要配合缓存、分片加载或调整item复用策略?
lovoydArkUI (方舟UI框架)
ArkUI的List组件在大量数据下滑动卡顿,如何优化?
子非鱼
仙银
你已经用了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固定高度。
尼克森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 布局代码和数据量级,帮你进一步定位。
Z用了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抓帧率就能看到优化效果。
AI助手List 组件在数千条数据下即使使用了 LazyForEach 仍掉帧,通常是因为懒加载只解决了"按需创建"的问题,而快速滑动时每个列表项仍在频繁走"创建-销毁"流程,同时缓存区、组件复用、数据更新粒度、布局层级等因素未做配套优化。完整的优化方案需要 LazyForEach + cachedCount 预加载 + @Reusable 组件复用 + 分页分片加载 + 布局精简多管齐下。
问题解释
LazyForEach 的作用是让 List 只构建可视区域内的组件,但它在组件滑出可视区域(含缓存区)后默认会销毁组件,滑入时再重新创建。当列表项结构较复杂(含图片、多层嵌套布局)时,这个反复创建和布局计算的开销就会直接体现为掉帧。因此官方推荐的组合是:
- 组件复用:给列表项的自定义组件加 @Reusable 装饰器,滑出屏幕的组件进入复用池而不是被销毁,滑入时通过 aboutToReuse 回调直接回填新数据,跳过整个创建流程。实测中组件创建次数会从"约等于数据条数"收敛到"一屏条数 + cachedCount"的常数级别。
- cachedCount 预加载:控制可视区域外上下额外预构建的节点数,避免滑动时"现创建"。默认值较小,一般建议设为单屏可见条数的 1
2 倍(38 之间),过大则浪费内存。 - 分片加载:数千条数据不建议一次性全部构造并塞入数据源,应在 onReachEnd 触底时分页追加,并通过 DataChangeListener 的细粒度通知(onDataAdd 等)告知框架,避免全量刷新。
- 键值稳定: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 更简洁且性能更好。
补充说明
除了上述核心手段,还有几个容易导致掉帧的细节需要排查:
- 数据更新粒度:不要用
this.dataSource = newDataSource这种整体替换的方式刷新数据,会导致整个列表重建、滚动位置丢失。应使用 onDataChange、onDataDelete、onDataAdd、onDataMove 等细粒度通知,只刷新变化的条目。 - reuseId 区分类型:如果列表中存在多种结构的列表项(如图片卡片和文字卡片),需要通过 reuseId 标记不同类型,避免不同结构的组件互相复用导致布局反复调整。
- 布局优化:减少列表项内部的嵌套层级;能固定高度的条目尽量固定高度,避免高度不确定导致反复测量;不要在 ListItem 内再嵌套可滚动容器;避免在 build 中做数据排序、过滤等复杂逻辑。
- 图片处理:列表项中的大图应使用异步解码和合适的采样尺寸,避免在滑动过程中同步解码大图阻塞主线程;可配合 PixelMap 做图片缓存。
- 组件冻结:配合自定义组件冻结机制,滑出缓存区的组件会被标记为 inactive,其内部的状态刷新和动画会被暂停,可进一步降低滑动时的负载。
- 效果验证:使用 DevEco Studio 的 Profiler 工具抓帧分析,重点观察丢帧率、组件创建次数和复用命中率。正常调优后,快速滑动时复用命中率应稳定在较高水平,来回滚动多趟组件创建次数基本不再增长。
总结来说,LazyForEach 只是长列表优化的第一步,需要同时配上 cachedCount 缓存、@Reusable 复用策略和分片加载,并保证 key 稳定、数据更新细粒度化,才能把数千条数据的滑动帧率稳定住。
匿名用户- 为了保障您的信息安全,请勿上传您的敏感个人信息(如您的密码等信息)和您的敏感资产信息(如关键源代码、签名私钥、调试安装包、业务日志等信息),且您需自行承担由此产生的信息泄露等安全风险。
- 如您发布的内容为转载内容,请注明内容来源。
我要提问题
HarmonyOS ArkUI_GestureRecognizer这个API生成tapGesture,手指按下去,移动一段距离抬起来仍会触发,如何规避?(API12+)
HarmonyOS 如何实现checkbox滑动多选(API12+)
HarmonyOS Slider组件如何自定义滑动时弹出的气泡框样式 (API12+)
HarmonyOS Swiper垂直滑动 和list垂直滑动组合使用,如何使list垂直滑动 不受swiper影响,并且list上推到底部继续上推 swiper就会切换,list下拉到顶部继续下拉会使swiper切换,这个如何实现? (API12+)
HarmonyOS ArkUI中使用Navigation进行画面迁移如何判断forward还是back (API12+)
HarmonyOS 如何实现色块的循环渐变而不是滑动 (API12+)
HarmonyOS 手势无法滑动外层Tabs组件(API12+)
HarmonyOS 页面滑动丢帧 (API12+)




































苏公安网备 32011402010933号