HarmonyOS 列表性能优化:@Reusable 组件复用让长列表丝滑滚动
·
HarmonyOS 列表性能优化:@Reusable 组件复用让长列表丝滑滚动
前言
List / Grid 是几乎所有 App 的主骨架。当数据上千条、且每个 ListItem 结构复杂(图片、富文本、嵌套布局)时,简单的 ForEach 会遇到两个致命问题:频繁创建销毁组件节点导致滚动掉帧、以及状态错乱(复用后旧数据没清空)。ArkUI 提供 @Reusable 装饰器 + aboutToReuse 生命周期,把「组件节点池化复用」这件事标准化。本文结合可运行示例,讲透 @Reusable 的原理、复用回调的正确写法、以及与 @State/@ObservedV2 的配合,帮你把长列表从「一滑就卡」优化到「丝般顺滑」。
问题描述
典型症状:
- 列表滚动到几百项后明显掉帧,Profiler 显示「组件创建」占比极高。
- 复用的
ListItem偶尔显示上一条的旧数据——因为没在复用回调里重置状态。 - 在
aboutToReuse里重新new了整个数据对象,反而比不复用还慢。 - 把
@Reusable加在了List上而不是ListItem内部的自定义组件上,完全没生效。
根因:ForEach 默认按「进屏创建、出屏销毁」管理节点;@Reusable 则让它「出屏回收、进屏复用」,省掉创建开销——但复用必须配合 aboutToReuse(params) 把新数据灌进来。
细节解析
1. @Reusable 作用的对象
- 必须加在被
ForEach/LazyForEach直接生成的子组件上(通常是封装好的ListItem内容组件),不是List本身。 - 配合
LazyForEach使用效果最佳:LazyForEach 负责「按需加载数据」,@Reusable 负责「节点复用」,两者是正交优化。
2. aboutToReuse 生命周期
- 组件从复用池取出、重新入屏前调用
aboutToReuse(params: object),参数来自reuseId+ 你this.item = params注入的数据。 - 这里是重置/更新状态的唯一正确位置,相当于「二次初始化」。
- 不要在这里
new大对象,直接把传入的字段赋值给@Trace/@State即可,框架会自动触发最小刷新。
3. reuseId 分组
- 列表里有多种「布局样式」时,用
reuseId区分,避免把 A 样式节点复用成 B 样式。 - 同一
reuseId的节点可互相复用;不同reuseId进不同池。
4. 与状态管理 V2 的配合
- 组件内用
@Trace标记会被复用时变化的字段,aboutToReuse里直接赋值,刷新粒度精确到字段。
示例代码(可运行 ArkTS/ArkUI)
示例 1:基础 @Reusable 列表项
// components/FeedItem.ets
@Reusable
@ComponentV2
struct FeedItem {
@Trace title: string = '';
@Trace cover: ResourceStr = $r('app.media.startIcon');
@Trace liked: boolean = false;
// 复用入屏前,框架用新数据调用此回调
aboutToReuse(params: Record<string, Object>): void {
this.title = params['title'] as string;
this.cover = params['cover'] as ResourceStr;
this.liked = params['liked'] as boolean;
}
build() {
Row({ space: 12 }) {
Image(this.cover).width(64).height(64).borderRadius(8)
Column({ space: 4 }) {
Text(this.title).fontSize(16).maxLines(2)
Text(this.liked ? '已赞' : '未赞').fontColor(this.liked ? '#0A59F7' : '#999')
}.alignItems(HorizontalAlign.Start)
}
.padding(12)
.width('100%')
}
}
示例 2:用 LazyForEach + 数据源驱动
// model/FeedDataSource.ets
import { BasicDataSource } from './BasicDataSource';
export class FeedDataSource extends BasicDataSource<Feed> {
// 继承官方 BasicDataSource,实现 getData/getCount/... 即可
}
export interface Feed {
id: number;
title: string;
cover: ResourceStr;
liked: boolean;
}
// pages/FeedList.ets
import { FeedItem } from '../components/FeedItem';
import { FeedDataSource, Feed } from '../model/FeedDataSource';
@Entry
@ComponentV2
struct FeedList {
private data: FeedDataSource = new FeedDataSource();
aboutToAppear() {
for (let i = 0; i < 1000; i++) {
this.data.pushData({ id: i, title: `动态 ${i}`, cover: $r('app.media.startIcon'), liked: false });
}
}
build() {
List() {
LazyForEach(this.data, (item: Feed) => {
ListItem() {
FeedItem()
.reuseId('feed') // 同一布局样式共用复用池
}
}, (item: Feed) => item.id.toString())
}
.cachedCount(5) // 预渲染前后各 5 个,进一步减少白屏
}
}
示例 3:多布局用 reuseId 分组
LazyForEach(this.data, (item: any) => {
ListItem() {
if (item.type === 'text') {
TextItem().reuseId('text')
} else {
ImageItem().reuseId('image')
}
}
}, (item) => item.id.toString())
总结
@Reusable加在列表项组件上,不是List上;配合LazyForEach效果最大化。aboutToReuse(params)是状态重置的唯一正确入口,把新数据灌进@Trace字段,框架自动最小刷新。- 不要在其中
new大对象,直接赋值即可,否则抵消复用收益。 - 多布局用
reuseId分组,避免样式串台。 cachedCount配合调优:缓存前后 N 个节点,减少滚动白屏与瞬时创建。- 验证方法:开启 DevEco Profiler 的「组件创建」帧,开启
@Reusable后该指标应显著下降,滚动 FPS 稳定贴近 60/120。
@Reusable 是长列表从「能滚」到「流畅」的分水岭。它改动极小(加一个装饰器 + 一个回调),收益却贯穿整个列表体验,是所有信息流、订单列表、聊天记录的必选项。
更多推荐



所有评论(0)