鸿蒙 ArkUI 长列表性能优化:从掉帧到稳定 60 帧的实践清单
长列表是鸿蒙应用里最容易出现掉帧的场景之一。本文从数据源、组件复用、状态粒度、布局和图片几个维度,整理一套可以直接落地的 ArkUI 长列表优化清单,适合已经能写出可运行页面、但列表滚动仍不够顺滑的开发者。
一、先确认问题出在哪
列表卡顿通常不是单一原因,建议先做一次简单定位:
- 首屏渲染慢:进入页面后长时间白屏或骨架屏停留过久;
- 滚动掉帧:滑动时明显顿挫,帧率波动大;
- 操作延迟:点击、切换状态后响应慢;
- 内存上涨:滑动越久内存越高,甚至触发回收。
对应到 ArkUI,常见根因是:一次性创建全部列表项、状态刷新范围过大、布局层级过深、图片解码占用主线程。先分清属于哪一类,再针对性处理。
二、用 LazyForEach 替代一次性 ForEach
ForEach 会在构建时创建全部子组件,数据量一大,首屏就会被拖慢。长列表应使用 LazyForEach,按需创建可见区域附近的节点。
LazyForEach 需要一个数据源实现 IDataSource:
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = [];
private data: FeedItemData[] = [];
totalCount(): number {
return this.data.length;
}
getData(index: number): FeedItemData {
return this.data[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
push(item: FeedItemData): void {
this.data.push(item);
this.listeners.forEach(l => l.onDataAdd(this.data.length - 1));
}
reset(items: FeedItemData[]): void {
this.data = items;
this.listeners.forEach(l => l.onDataReloaded());
}
}
页面里这样使用:
LazyForEach(this.source, (item: FeedItemData) => {
FeedItem({ item: item })
}, (item: FeedItemData) => item.id)
第三个参数是键生成函数,必须保证唯一且稳定。用数组下标做键,在插入或删除时会导致复用错位。
三、开启组件复用
LazyForEach 解决的是“按需创建”,组件复用解决的是“重复创建”。使用 @Reusable 装饰列表项组件,配合 aboutToReuse 更新数据:
@Reusable
@Component
struct FeedItem {
@State item: FeedItemData = { id: '', title: '', cover: '', summary: '' };
aboutToReuse(params: Record<string, Object>): void {
this.item = params.item as FeedItemData;
}
build() {
Row() {
Image(this.item.cover)
.width(72)
.height(72)
.objectFit(ImageFit.Cover)
Column() {
Text(this.item.title)
.fontSize(16)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.item.summary)
.fontSize(13)
.fontColor('#666666')
.maxLines(1)
}
.layoutWeight(1)
.margin({ left: 12 })
}
.width('100%')
.padding(12)
}
}
注意两点:
- 复用组件内部不要保存只属于某一条数据的临时状态;
aboutToReuse里只做数据赋值,不要做网络请求或复杂计算。
四、缩小状态刷新范围
列表项里用了 @State,任何一次赋值都会触发该组件重新渲染。常见误区是把整个列表数据放在页面级 @State 里,然后每次更新都整体替换,导致整页刷新。
更稳妥的做法:
- 列表数据源交给
LazyForEach的IDataSource管理; - 单个列表项内部状态用
@State,只影响自身; - 跨组件共享且需要观察的对象用
@Observed与@ObjectLink,避免深层对象整体刷新; - 只用于展示、不会变化的字段,不要加状态装饰器。
五、控制布局层级
ArkUI 的布局计算与层级深度相关。列表项里嵌套过多 Column、Row、Stack,单个节点开销不大,但乘以可见数量后就会明显。
几个实用原则:
- 能用
Row、Column直接排布的,不要额外包一层容器; - 固定宽高的图片和图标显式设置尺寸,减少测量;
- 列表项高度尽量一致,避免频繁重新测量;
- 避免在
build中做字符串拼接、排序、过滤等计算,提前在数据层处理好。
六、图片是最大的隐形开销
图片解码和内存占用往往是掉帧的主要来源。建议:
- 列表缩略图使用合适尺寸,不要直接加载原图;
Image显式设置宽高,避免布局阶段反复调整;- 快速滑动场景谨慎使用同步加载,优先让图片异步加载;
- 长列表项离开可视区域后及时释放大图引用;
- 头像、图标这类小图尽量复用同一资源对象。
七、动效要克制
animateTo 和属性动画能提升体验,但在滚动过程中对大量节点同时做动画,会明显增加渲染压力。
建议只在交互反馈上使用短时动画,列表滚动期间不加额外动效;删除、插入等操作可以使用局部动画,不要对整列表做统一过渡。
八、验收指标
优化是否有效,不要只靠“感觉顺了”,建议记录三个指标:
- 首屏可见时间:从进入页面到首屏内容出现;
- 滚动帧率:连续滑动 10 秒,观察是否稳定在接近 60 帧;
- 内存曲线:反复上下滑动后,内存是否持续上涨。
可以用 DevEco Studio 自带的 ArkUI Inspector 查看组件树和布局层级,用性能分析器观察帧率与耗时。重点看两件事:可见区域外的节点是否仍在创建,以及单帧耗时是否集中在布局或图片解码。
九、小结
长列表优化的顺序建议是:先换 LazyForEach,再开组件复用,然后缩小状态范围,最后处理布局和图片。每一步都能独立验证效果,不要一次性全改,否则很难判断是哪一项起了作用。
真正稳定的列表,通常不是靠某一个“高级 API”,而是靠数据源、复用、状态和资源四条线同时收敛。先把这四件事做扎实,再考虑更复杂的预加载和缓存策略。
- 点赞
- 收藏
- 关注作者
评论(0)