系列文章《鸿蒙开发从入门到专家》第 52 篇 · 高级
上一篇讲启动优化,这篇讲列表渲染。两者并列应用启动后最常见的性能瓶颈。
LazyForEach 是 ArkUI 提供的懒加载列表组件,只在可视区域内渲染列表项,屏幕外的组件不创建。听起来很美好,但实际项目里 400 条数据还是会卡。问题出在缓存策略、数据源实现和组件复用上。
一、LazyForEach 的渲染机制
先搞清楚 LazyForEach 到底做了什么。
普通 ForEach 会在 build 时一次性创建所有列表项组件。100 条数据就创建 100 个组件实例,1000 条就 1000 个。内存爆炸,渲染卡顿。
LazyForEach 的做法是只渲染可视区域内的 n 条数据,外加缓存区域的 cachedCount 条。当用户滑动时,滑出屏幕的组件被回收,新进入可视区域的组件被复用。
渲染过程:
首次加载:请求 n + cachedCount 条数据,屏幕上显示 n 条。
用户滑动:缓存区域的组件从屏外进入可视区,直接渲染,不需要重新创建组件。
继续滑动:屏外缓存的列表项数量少于 cachedCount 时,触发新一轮数据加载,预加载 cachedCount 条。
双向滑动:上下两个方向分别缓存 cachedCount 条数据。
默认 cachedCount 为 1,几乎等于没缓存。
二、数据源实现
LazyForEach 要求传入一个实现了 IDataSource 接口的数据源。这是最容易出错的地方。
import { LazyForEach } from '@kit.ArkUI';
interface IDataChangeListener {
onDataAdded(index: number): void;
onDataMoved(from: number, to: number): void;
onDataDeleted(index: number): void;
onDataChanged(index: number): void;
}
interface IDataSource {
totalCount(): number;
getData(index: number): Object;
registerDataChangeListener(listener: DataChangeListener): void;
unregisterDataChangeListener(listener: DataChangeListener): void;
}
class SimpleDataSource implements IDataSource {
private data: string[] = [];
private listeners: DataChangeListener[] = [];
totalCount(): number {
return this.data.length;
}
getData(index: number): string {
return this.data[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
this.listeners.push(listener);
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
// 数据操作时必须通知监听器
pushData(item: string): void {
const index = this.data.length;
this.data.push(item);
this.listeners.forEach(listener => {
listener.onDataAdded(index);
});
}
reloadData(newData: string[]): void {
this.data = newData;
this.listeners.forEach(listener => {
listener.onDataReloaded();
});
}
}
关键点:pushData 时必须调用 listener.onDataAdded(index),否则列表不会更新。这是新手最常犯的错误——数据源数组变了,UI 不刷新。
三、cachedCount 怎么设
cachedCount 不是越大越好。它直接影响内存占用和滑动流畅度。
经验公式:cachedCount = 一屏可见项数 / 2。如果一屏显示 8 条,cachedCount 设 4。
但如果列表项包含网络图片加载,可以适当增大 cachedCount,给图片更多预加载时间。如果列表项包含视频或大图,减小 cachedCount 控制内存。
@Entry
@Component
struct ListPage {
private dataSource: SimpleDataSource = new SimpleDataSource();
aboutToAppear(): void {
for (let i = 0; i < 1000; i++) {
this.dataSource.pushData(`Item ${i}`);
}
}
build() {
List() {
LazyForEach(this.dataSource, (item: string) => {
ListItem() {
Text(item)
.width('100%')
.height(60)
.textAlign(TextAlign.Center)
}
}, (item: string) => item)
}
.cachedCount(5)
.height('100%')
}
}
第三个参数是 keyGenerator,用于组件复用。返回唯一 key 帮助框架判断哪些列表项可以复用组件。不写也行,但性能会差一些。
四、动态预加载:Prefetcher
API 12 引入了 Prefetcher 机制,比 cachedCount 更智能。cachedCount 是固定数量的预渲染,Prefetcher 能自适应调整预取范围、优先加载可视区域、在快速滚动时智能取消不必要任务。
import { LazyForEachOperation, IDataSourcePrefetching } from '@kit.ArkUI';
class PrefetchDataSource implements IDataSourcePrefetching {
private data: string[] = [];
totalCount(): number {
return this.data.length;
}
getData(index: number): string {
return this.data[index];
}
// Prefetcher 调用:预取数据
prefetch(index: number): void {
// 提前发起网络请求或解码图片
if (index < this.data.length) {
// 预取逻辑
}
}
// 取消预取(快速滑动时框架会取消不必要任务)
cancelPrefetch(index: number): void {
// 取消正在进行的预取
}
}
Prefetcher 的优势在于"预取"和"预渲染"分离:预取只加载数据(如发网络请求),预渲染才创建组件。快速滑动时先预取,滑动减速后再预渲染,避免无意义的组件创建销毁。
五、组件复用:ReusableCustomNode
列表项复杂时(比如每个项包含图片+标题+副标题+操作按钮),反复创建和销毁组件开销很大。ArkUI 提供了组件复用机制:
@Reusable
@Component
struct ArticleItem {
@State item: ArticleData = new ArticleData();
aboutToReuse(params: Record<string, Object>): void {
this.item = params['item'] as ArticleData;
}
build() {
Row() {
Image(this.item.coverUrl)
.width(80)
.height(80)
Column() {
Text(this.item.title)
.fontSize(16)
.maxLines(1)
Text(this.item.summary)
.fontSize(12)
.maxLines(2)
}
.layoutWeight(1)
Image($r('app.media.ic_arrow'))
.width(16)
.height(16)
}
.padding(12)
}
}
使用 @Reusable 装饰器后,组件被回收时会进入复用池,而不是被销毁。新的列表项需要创建组件时,框架先从复用池取,通过 aboutToReuse 更新数据,跳过完整的组件创建流程。
在列表中使用:
List() {
LazyForEach(this.dataSource, (item: ArticleData) => {
ListItem() {
ArticleItem({ item: item })
}
}, (item: ArticleData) => item.id)
}
.cachedCount(5)
六、长列表极限场景
6.1 万条数据
10000 条数据 + LazyForEach + cachedCount(5) + 组件复用 = 可以跑。实测在 API 12 的模拟器上,首次进入页面加载 15 条(可视 8 条 + 缓存 7 条),滑动帧率稳定。
但有几个注意点:
keyGenerator 必须稳定。 如果 keyGenerator 返回的 key 不稳定(比如用 index 做 key),删除/插入数据时组件复用会出错。
// 正确:用数据唯一 ID
(item: ArticleData) => item.id
// 错误:用数组 index
(item: ArticleData, index: number) => index.toString()
避免在闭包中捕获大对象。 itemGenerator 是一个闭包,如果在里面引用外部的大数组,即使组件被回收,大对象也无法被 GC 回收。
// 有泄漏风险
const largeData = loadLargeData();
LazyForEach(source, (item) => {
processWith(largeData); // largeData 被闭包持有
});
// 正确做法:把大数据拆小后传入数据源
6.2 分页加载
下拉加载更多数据时,往数据源追加数据并通知监听器:
class PageDataSource implements IDataSource {
private data: ArticleData[] = [];
private listeners: DataChangeListener[] = [];
private loading: boolean = false;
totalCount(): number {
return this.data.length + (this.loading ? 1 : 0); // +1 给加载中占位
}
getData(index: number): ArticleData | string {
if (index >= this.data.length) return 'loading';
return this.data[index];
}
appendData(newData: ArticleData[]): void {
const startIndex = this.data.length;
this.data = this.data.concat(newData);
this.listeners.forEach(listener => {
listener.onDatasetChange([{
type: DataOperationType.ADD,
index: startIndex,
count: newData.length
}]);
});
}
// ... registerDataChangeListener / unregisterDataChangeListener 省略
}
在 List 组件中监听到达底部:
List() {
LazyForEach(this.dataSource, (item: ArticleData | string) => {
ListItem() {
if (typeof item === 'string') {
Text('加载中...')
} else {
ArticleItem({ item: item })
}
}
}, (item: ArticleData | string) => {
return typeof item === 'string' ? 'footer' : item.id;
})
}
.onReachEnd(() => {
this.loadMore();
})
七、性能对比数据
在模拟器上对 1000 条纯文本列表的测试结果:
ForEach 全量渲染在 1000 条数据时直接卡死 3 秒以上,内存暴涨。LazyForEach 配合合理的 cachedCount 和组件复用,能保持稳定帧率和可控内存。
小结
LazyForEach 的核心收益是只渲染可视区域 + 缓存区的组件。要用好它:
正确实现 IDataSource,数据变化时通知监听器。
根据列表项复杂度调 cachedCount,简单项设 5-10,复杂项设 3-5。
复杂列表项用
@Reusable装饰器做组件复用。大数据量场景用稳定 keyGenerator,避免闭包捕获大对象。
API 12 以上考虑 Prefetcher 做智能预加载。
下篇讲动画系统。animateTo 和 animation 怎么选、曲线怎么配、复杂交互场景怎么编排。
参考阅读:
第 8 篇:鸿蒙性能优化实战(列表渲染部分)
第 7 篇:鸿蒙自定义组件与动画
官方文档:LazyForEach、IDataSource