五百条截图列表滑成幻灯片,LazyForEach 配错了复用等于白搭
长列表卡顿的根因只有一句话
截图整理工具的分类结果页,五百条截图卡片,每张卡片带缩略图、OCR 摘要和分类标签。用 ForEach 直接渲染,滑到第三屏就开始明显掉帧。换成 LazyForEach 之后好了一些,但快速滑动仍然会白块,偶尔还卡住。
LazyForEach 只解决了“按需创建”,离屏的组件滑出去就销毁,上屏的重新 build。复杂卡片每次 build 都要重新布局、重新解码图片,创建开销并没有被摊薄。真正让长列表流畅的,是懒加载、组件复用、预加载、状态下沉四条线一起守住。

@Reusable 让组件实例活过离屏,aboutToReuse 必须接住新数据
给卡片组件加上 @Reusable,ArkUI 会把滑出屏幕的组件实例放进复用池,新数据上屏时直接复用这个实例,只回调 aboutToReuse 更新数据,省掉了 build() 从头构建的开销。这对带图片和嵌套布局的复杂卡片收益最大。
typescript
@Reusable
@ComponentV2
struct ScreenshotCard {
@Local item: ScreenshotItem = {} as ScreenshotItem;
@Local selected: boolean = false;
aboutToReuse(params: Record<string, Object>): void {
this.item = params.item as ScreenshotItem;
this.selected = false; // 交互状态也要重置
}
build() {
Column() {
Image(this.item.thumbUri).width('100%').aspectRatio(1.5)
Text(this.item.ocrSummary).maxLines(2)
TagChip({ text: this.item.category })
}
}
}
aboutToReuse 是复用的唯一数据入口。所有会被 UI 读取的 @Local 变量,都必须在这里按新数据重新赋值。普通成员变量在复用时不会走状态更新,也不受参数覆盖,需要手动赋值。常见错误是只更新了数据字段,漏掉了 selected、expanded 这类交互态,用户滑回来发现上一张卡的选中状态跟着过来了。
cachedCount 不是越大越好,从较小值开始调
cachedCount 控制列表预加载的缓存项数量。设小了,快速滑动时来不及创建组件,出现白块;设大了,内存占用抬升,GC 压力增加,反而可能引入卡顿。建议从较小值开始,结合真机 Profiler 在“白块、内存、帧耗时”之间调平衡。
typescript
List() {
LazyForEach(this.dataSource, (item: ScreenshotItem) => {
ListItem() {
ScreenshotCard({ item: item })
}
}, (item: ScreenshotItem) => item.id)
}
.cachedCount(3) // 先给 3,用 Profiler 看再调
LazyForEach 的 key 生成函数必须返回稳定且唯一的值。用 index 做 key 在列表增删时会触发大面积复用错位,卡片数据串到别的 item 上。用业务 ID 做 key,复用的对应关系才准确。
列表数据用 @State 管集合,单条状态下沉到卡片内部
一个常见的性能反模式是把整个列表数组的每一项都挂上深层 @Trace,任何一条数据的任何字段变化都可能触发列表级别的重新 diff。正确的分层是:列表容器只持有集合引用,单条 item 的选中、展开、点赞等局部状态下沉到卡片组件自己的 @Local 里。
如果列表项数据类用 @ObservedV2,只给参与 UI 的字段加 @Trace,不要把整个类打满。高频变化的字段(比如未读数)单独拆成独立的类,减少 diff 的范围。五百条截图的列表,LazyForEach + @Reusable + cachedCount + 状态下沉四条做到,中端机型的滑动掉帧能从个位数压到一两帧以内,体验差距是肉眼可见的。
更多推荐
所有评论(0)