HarmonyOS 组件封装与复用最佳实践(下篇)
组件复用是说,自定义组件从组件树上移除后,会被放入缓存池。后续创建相同类型的组件节点时,直接从缓存池里拿,不用重新创建。避免频繁创建和销毁对象,减少内存回收的频率复用缓存中的组件可以直接绑定数据,降低了计算开销最典型的应用场景就是长列表滑动。你想想,一个列表有几百条数据,用户快速滑动的时候,如果每个列表项都要创建销毁,那肯定卡。组件封装和复用这事儿,说难不难,说简单也不简单。难的地方在于,你得理解
写给那些在 ArkUI 里摸爬滚打的开发者们
三、组件复用:让列表滑动更流畅
3.1 什么是组件复用
组件复用是说,自定义组件从组件树上移除后,会被放入缓存池。后续创建相同类型的组件节点时,直接从缓存池里拿,不用重新创建。

这事儿的好处很明显:
- 避免频繁创建和销毁对象,减少内存回收的频率
- 复用缓存中的组件可以直接绑定数据,降低了计算开销
最典型的应用场景就是长列表滑动。你想想,一个列表有几百条数据,用户快速滑动的时候,如果每个列表项都要创建销毁,那肯定卡。
3.2 复用的核心机制
ArkUI 提供了 @Reusable 装饰器来实现组件复用。原理是这样的:

- 标记了
@Reusable的自定义组件,在滑动出屏幕一定范围后,从组件树上被移除,组件对象实例被放入 CustomNode 虚拟结点 - 列表的 RecycleManager 将这些 CustomNode 虚拟结点回收,根据复用标识
reuseId分组,形成缓存池 - 继续滑动,新的列表项需要显示时,RecycleManager 优先从缓存池中查找对应
reuseId的视图对象,然后绑定新数据,重用该节点
3.3 怎么实现组件复用
实现组件复用就三步:
第一步:用@Reusable 装饰器修饰可复用的自定义组件
@Reusable
@Component
struct ReusableComponent {
@State text: string = ''
aboutToReuse(params: Record<string, Object>): void {
this.text = params.text as string;
}
build() {
Text(this.text)
}
}
第二步:实现 aboutToReuse() 生命周期回调
当组件从缓存中重新加入到节点树时,会触发这个回调。组件的构造参数会传递进来,你在这里处理数据刷新。
第三步:设置 reuseId
@Entry
@Component
struct Index {
build() {
Column() {
ReusableComponent({ text: 'hello' })
.reuseId('my_component')
}
}
}
reuseId 用来划分组件的复用组别。没设置的话,组件名会默认作为 reuseId。
3.4 注意事项
@Reusable修饰的组件需要布局在同一个父自定义组件下才能实现缓存复用- 不建议在
@Reusable修饰的组件中嵌套使用另一个@Reusable组件
3.5 同一列表内的组件复用
场景一:列表项结构类型相同
这种场景最简单,列表中的每一项都是由相同类型的元素和布局构成。

@Reusable
@Component
struct ItemView {
@State title: string | Resource = '';
@State from: string | Resource = '';
@State tail: string | Resource = '';
aboutToReuse(params: Record<string, Object>): void {
this.title = params.title as string;
this.from = params.from as string;
this.tail = params.tail as string;
}
build() {
// 列表项布局
}
}
@Component
export struct OneTypeItemPage {
build() {
NavDestination() {
Column() {
List() {
LazyForEach(this.dataSource, (item: ItemData) => {
ItemView({ title: item.title, from: item.from, tail: item.tail })
.reuseId('item_id')
}, (item: ItemData) => item.id.toString())
}
}
}
}
}
场景二:列表项结构类型不同
列表中有多种类型的列表项,比如文本、单图、多图等。


@Reusable
@Component
struct TextTypeItemView { /* ... */ }
@Reusable
@Component
struct ImageTypeItemView { /* ... */ }
@Reusable
@Component
struct ThreeImageTypeItemView { /* ... */ }
@Component
export struct MultiTypeItemPage {
build() {
NavDestination() {
Column() {
List() {
LazyForEach(this.dataSource, (item: ItemData) => {
if (item.type === 0) {
TextTypeItemView({ item: item }).reuseId('text_item_id')
} else if (item.type === 1) {
ImageTypeItemView({ item: item }).reuseId('image_item_id')
} else if (item.type === 2) {
ThreeImageTypeItemView({ item: item }).reuseId('three_image_item_id')
}
}, (item: ItemData) => item.id.toString())
}
}
}
}
}
场景三:列表项内子组件可拆分组合
列表项有多种结构类型,但内部子组件可以拆分组合。比如顶部都是文本标题,底部都是发布时间,中间区域有单图、多图、视频三种情况。
关键点:用@Builder 实现,不要用@Component 嵌套子组件
因为缓存池位于自定义组件上,嵌套子组件后会把缓存池分割,导致复用不生效。而使用 @Builder 可以使内部的自定义组件依然汇聚在同一个缓存池里。
正例:
@Reusable
@Component
struct TopView { /* ... */ }
@Reusable
@Component
struct BottomView { /* ... */ }
@Reusable
@Component
struct MiddleSingleImageView { /* ... */ }
@Reusable
@Component
struct MiddleThreeImageView { /* ... */ }
@Reusable
@Component
struct MiddleVideoView { /* ... */ }
@Component
export struct ComposableItemPage {
@Builder
itemBuilderSingleImage(item: ItemData) {
TopView({ item: item }).reuseId('top_id')
MiddleSingleImageView({ item: item }).reuseId('middle_image_id')
BottomView({ item: item }).reuseId('bottom_id')
}
@Builder
itemBuilderThreeImage(item: ItemData) {
TopView({ item: item }).reuseId('top_id')
MiddleThreeImageView({ item: item }).reuseId('middle_three_image_id')
BottomView({ item: item }).reuseId('bottom_id')
}
build() {
NavDestination() {
Column() {
List() {
LazyForEach(this.dataSource, (item: ItemData) => {
ListItem() {
Column() {
if (item.type === 0) {
this.itemBuilderSingleImage(item)
} else if (item.type === 1) {
this.itemBuilderThreeImage(item)
}
}
}
}, (item: ItemData) => item.id.toString())
}
}
}
}
}
反例(不要用):
// 错误:@Component 嵌套会分割缓存池
@Component
struct SingleImageComponent{
build() {
Column() {
TopView({ item: item }).reuseId('top_id')
MiddleSingleImageView({ item: item }).reuseId('middle_image_id')
BottomView({ item: item }).reuseId('bottom_id')
}
}
}
3.6 多个列表间的组件复用
应用里有不同的标题页面,每个页面下有个列表。页面切换时,列表与列表之间如果存在结构相同的列表项,就有组件复用的优化可能。

解决方案是自定义一个全局的复用缓存池 NodePool,利用 BuilderNode 的节点复用能力,根据页面状态创建、回收、复用子组件。
核心实现:
// 列表项占位结点类
export class NodeItem extends NodeController {
public builder: WrappedBuilder<ESObject> | null = null;
public node: BuilderNode<ESObject> | null = null;
public data: ESObject = {};
public type: string = '';
aboutToDisappear(): void {
NodePool.getInstance().recycleNode(this.type, this);
}
makeNode(uiContext: UIContext): FrameNode | null {
if (!this.node) {
this.node = new BuilderNode(uiContext);
this.node.build(this.builder, this.data);
}
return this.node.getFrameNode();
}
}
// 全局复用缓存池
export class NodePool {
private static instance: NodePool;
private nodePool: HashMap<string, LinkedList<NodeItem>>;
private constructor() {
this.nodePool = new HashMap();
}
public static getInstance() {
if (!NodePool.instance) {
NodePool.instance = new NodePool();
}
return NodePool.instance;
}
public getNode(type: string, item: ESObject, builder: WrappedBuilder<ESObject>): NodeItem {
// 从缓存池查找可复用节点
// 如果找不到,创建新节点
}
public recycleNode(type: string, node: NodeItem) {
// 回收到缓存池
}
}
3.7 使用 onIdle() 预创建组件
首次进入页面时,复用缓存池是空的,所有列表项都要新创建,这会导致耗时较高。

可以用 onIdle() 预创建组件,把组件对象提前放入复用缓存池中。
export class IdleCallback extends FrameCallback {
private uiContext: UIContext;
private todoCount: number = 0;
private dataArray: ItemData[] = [];
constructor(context: UIContext, preBuildData: ItemData[]) {
super();
this.uiContext = context;
this.dataArray = preBuildData;
}
onIdle(idleTimeInNano: number): void {
// 利用空闲时间预创建组件
while (timeLeft >= 1000000) { // 1ms
NodePool.getInstance().preBuild('reuse_type_', this.dataArray[this.todoCount], listItemWrapper, this.uiContext);
this.todoCount++;
if (this.todoCount >= this.dataArray.length) return;
}
this.uiContext.postFrameCallback(this);
}
}
@Entry
@Component
struct Index {
aboutToAppear(): void {
let dataArray: ItemData[] = genMockItemData(100);
this.getUIContext().postFrameCallback(new IdleCallback(this.getUIContext(), dataArray));
}
}
3.8 更多优化方法
使用 attributeUpdater 实现组件属性的部分刷新
在可复用组件中使用 attributeUpdater 可以控制指定属性的刷新,避免不必要的重绘。
export class MyTextUpdater extends AttributeUpdater<TextAttribute> {
private color: string | number | Resource | Color = '';
constructor(color: string | number | Resource | Color) {
super();
this.color = color;
}
initializeModifier(instance: TextAttribute): void {
instance.fontColor(this.color)
}
}
@Reusable
@Component
export struct OneMoment {
@State text: string = '';
color: string | number | Resource | Color = '';
textUpdater: MyTextUpdater | null = null;
aboutToAppear(): void {
this.textUpdater = new MyTextUpdater(this.color);
}
aboutToReuse(params: Record<string, Object>): void {
this.color = params.color as string;
this.text = params.text as string;
this.textUpdater?.attribute?.fontColor(this.color);
}
build() {
Column() {
Text(this.text)
Text('desc').attributeModifier(this.textUpdater)
}
}
}
使用@Link/@ObjectLink 替代@Prop 以减少深拷贝
在可复用组件中,建议用 @Link/@ObjectLink 替代 @Prop。因为 @Prop 装饰变量时会进行深拷贝,增加了创建时间及内存消耗。
反例:
@Reusable
@Component
export struct OneMoment {
@Prop moment: FriendMoment; // 深拷贝
}
正例:
@Reusable
@Component
export struct OneMoment {
@ObjectLink moment: FriendMoment; // 共享地址
}
避免对自动更新的状态变量在 aboutToReuse() 中重复赋值
如果可复用组件中使用了 @Link/@ObjectLink/@Prop 等自动同步父子组件数据的状态变量,则不需要在 aboutToReuse() 中对这些数据重复赋值。
反例:
@Reusable
@Component
export struct OneMoment {
@ObjectLink moment: FriendMoment;
aboutToReuse(params: Record<string, Object>): void {
this.moment.id = (params.moment as FriendMoment).id // 多余!
this.moment.userName = (params.moment as FriendMoment).userName // 多余!
}
}
正例:
@Reusable
@Component
export struct OneMoment {
@ObjectLink moment: FriendMoment;
aboutToReuse(params: Record<string, Object>): void {
// 不需要重复赋值
}
}
使用 reuseId 标记布局发生变化的组件
在同一段自定义组件代码中,如果使用 if/else 条件语句控制布局结构,会导致在不同逻辑分支中创建不同布局的组件。此时可以用 reuseId 来区分发生变化的分支逻辑。
@Entry
@Component
struct WithReuseId {
build() {
Column() {
List() {
LazyForEach(this.momentData, (moment: FriendMoment) => {
ListItem() {
OneMoment({ moment: moment })
.reuseId((moment.image !== '') ? 'withImage_id' : 'noImage_id')
}
}, (moment: FriendMoment) => moment.id)
}
}
}
}
@Reusable
@Component
export struct OneMoment {
@ObjectLink moment: FriendMoment;
build() {
Column() {
Text(this.moment.text)
if (this.moment.image !== '') {
Flex({ wrap: FlexWrap.Wrap }) {
Image($r(this.moment.image))
}
}
}
}
}
3.9 如何检查组件复用是否生效
有三种方法:
- 使用 Code Linter 扫描工具进行代码检查,重点关注
@performance/hp-arkui-use-reusable-component规则 - 通过 Profiler 工具抓取 Trace,搜索组件名称,根据
BuildRecycle字段识别是否触发复用渲染 - 通过 Profiler 工具抓取 Trace,识别是否发生丢帧,判断子组件创建的次数
四、组件复用问题诊断与分析
4.1 核心概念回顾
组件复用的本质是自定义组件的缓存池。使用 @Reusable 装饰器标记自定义组件后,当该组件进入销毁流程时,将改为进入缓存池。当需要创建此自定义组件时,优先尝试从缓存池中寻找对应 reuseId 的自定义组件,若未找到再进行创建。
此缓存池的实例位于 @Reusable 标记的自定义组件的父自定义组件上,会随父自定义组件的销毁一并销毁。


关键概念:
- 缓存池:用于存放自定义组件,位于父自定义组件对象上
- @Reusable 装饰器:标记自定义组件,使其在释放时进入缓存池
- reuseId:自定义组件在缓存池中的键,用于区分池中的自定义组件类型
4.2 未使用组件复用的场景

场景一:列表项结构类型相同
反例:
@Component // 缺少@Reusable
struct NewsContent {
@Builder
myBuilder(item: ItemData) {
TopView({ item: item })
MiddleSingleImageView({ item: item })
BottomView({ item: item })
}
}
优化建议:给父组件 NewsContent 添加 @Reusable 装饰器。
场景二:列表项结构类型不同

子场景①:"总 - 分式"子组件可变结构
反例:
@Component // 缺少@Reusable 和 reuseId
struct NewsContent {
@Builder
myBuilder(item: ItemData) {
TopView({ item: item })
if (item.type === 0) {
MiddleSingleImageView({ item: item })
} else if (item.type === 1) {
MiddleThreeImageView({ item: item })
}
BottomView({ item: item })
}
}
优化建议:添加 @Reusable 装饰器,同时根据 if-else 分支合理配置 reuseId。
正例:
List() {
LazyForEach(this.dataSource, (item: ItemData) => {
ListItem() {
NewsContent({ item: item }).reuseId(`${item.type}`)
}
}, (item: ItemData) => item.id.toString())
}
@Reusable
@Component
struct NewsContent { /* ... */ }
场景三:列表项内子组件可拆分组合
反例:
@Builder
itemBuilderVideo(item: ItemData) {
Column() {
TopView({ item: item }) // 缺少@Reusable
MiddleVideoView({ item: item }) // 缺少@Reusable
BottomView({ item: item }) // 缺少@Reusable
}
}
优化建议:将 @Builder 内的自定义组件添加 @Reusable 修饰。
4.3 组件复用使用不当的场景

场景一:父组件未使用复用,子组件使用复用
反例:
@Component // 父组件未复用
struct NewsContent {
@Builder
myBuilder(item: ItemData) {
TopView({ item: item }) // 子组件复用
}
}
@Reusable
@Component
struct TopView { /* ... */ }
问题:父组件未使用复用,但子组件使用了复用,会导致复用机制失效。因为父组件销毁时,子组件及对应的复用池也随之销毁。
优化建议:提升复用层级,在父组件启用复用,取消子组件复用。
场景二:复用嵌套
反例:
@Reusable
@Component
struct NewsContent {
@Builder
myBuilder(item: ItemData) {
TopView({ item: item }) // 子组件也复用了
}
}
@Reusable
@Component
struct TopView { /* ... */ }
问题:父组件和子组件都用了 @Reusable,形成"复用嵌套",会导致复用机制失效。
优化建议:父组件已启用复用,子组件就不要再复用了。
场景三:reuseId 分类过粗
反例:
@Reusable
@Component
struct NewsContent {
@Builder
myBuilder(item: ItemData) {
TopView({ item: item })
if (item.type === 0) {
MiddleSingleImageView({ item: item })
} else if (item.type === 1) {
MiddleThreeImageView({ item: item })
} else {
MiddleVideoView({ item: item })
}
BottomView({ item: item })
}
}
// 未设置 reuseId,默认使用组件名
问题:未根据 if-else 分支中的不同类型设置差异化的 reuseId,导致复用分类粒度过粗。
优化建议:应根据组件的结构、布局和特征,为不同类别分别配置独立稳定的 reuseId。
场景四:reuseId 分类过细
反例:
List() {
LazyForEach(this.dataSource, (item: ItemData) => {
ListItem() {
NewsContent({ item: item }).reuseId(`${item.type}`) // 每种类型都独立
}
}, (item: ItemData) => item.id.toString())
}
问题:reuseId 设置过于细化,会导致复用缓存池被过度拆分,形成多个小型缓存桶,降低组件复用率。
优化建议:将结构相近的列表项合并到一个类型中,以提升复用的命中率。
4.4 复用缓存池为空的情况
列表的首次滑动
在列表首次滑动过程中,复用缓存池为空属于正常现象。初始渲染时,所有可见组件均需通过新建(BuildItem)方式创建组件实例,尚未有组件被移出屏幕并回收至缓存池。
列表滑动速度突然变快
当列表项快速滑动时,若观察到大量组件以 BuildItem 方式创建,且复用行为(BuildRecycle)显著减少或消失,表明复用机制未能及时响应高频滚动事件。
可能出现的问题:
| 问题 | 原因 | 表现 |
|---|---|---|
| 回收滞后 | 滚动速度过快,系统来不及回收移出的组件 | 缓存池无法填充,新项只能通过新建方式进行创建 |
| 缓存池溢出 | 短时间内产生大量待回收项,超出缓存容量 | 旧实例被丢弃,新项仍需重建 |
4.5 组件复用后的卡顿问题

复用过程函数耗时长
在复用过程中会触发应用复用生命周期回调和内容刷新,若复用成功但仍有丢帧时,优先检查复用过程中是否存在耗时函数。
典型 Trace 分析中,BuildRecycle 是典型的复用成功回调,此处执行耗时过长造成了应用丢帧。
复用过程刷新耗时长
复用过程的更新与状态管理的标准更新一致,可参考状态管理最佳实践文档。
五、避坑指南
写了这么多,最后总结几个最容易踩的坑:
坑一:忘了加@Reusable
这是最常见的。你把组件封装得好好的,列表一滑动就卡。一查,忘了加 @Reusable 装饰器。
记住:只要是想复用的自定义组件,必须加 @Reusable。
坑二:reuseId 设置不当
reuseId 设置过粗,不同类型的组件混在一起复用,会导致显示错误。
reuseId 设置过细,每种类型都单独一个 reuseId,会导致缓存池被过度拆分,复用率降低。
怎么把握这个度?看组件的结构差异。结构差异大的,分开;结构差异小的,合并。
坑三:复用嵌套
父组件用了 @Reusable,子组件也用了 @Reusable。这看起来挺合理,但实际上会导致复用机制失效。
记住:父组件已经复用,子组件就不要再复用了。
坑四:在 aboutToReuse() 中重复赋值
@Link/@ObjectLink/@Prop 等状态变量会自动同步父子组件数据,不需要在 aboutToReuse() 中重复赋值。重复赋值会导致额外的计算更新耗时。
坑五:用函数方法作为复用组件的入参
可复用组件的入参如果用了函数方法,每次复用都会执行这个函数,造成性能问题。
解决办法:先把函数执行结果存到状态变量里,然后传状态变量。
坑六:在@Builder 里嵌套@Component
列表项内子组件可拆分组合的场景下,如果用 @Component 嵌套子组件,会把缓存池分割,导致复用不生效。
正确做法:用 @Builder 实现,让内部的自定义组件汇聚在同一个缓存池里。
坑七:首次滑动卡顿
列表首次滑动时,复用缓存池是空的,所有组件都要新建,这会导致卡顿。
解决办法:用 onIdle() 预创建组件,把组件提前放入缓存池。
六、最后说两句
组件封装和复用这事儿,说难不难,说简单也不简单。
难的地方在于,你得理解 ArkUI 的复用机制,知道缓存池是怎么工作的,知道 reuseId 是怎么分组的。
简单的地方在于,一旦你理解了这些,用起来其实就那几招:加 @Reusable、设 reuseId、实现 aboutToReuse()。
但真正要做好,还得在实际项目中多练。每个项目的情况都不一样,列表项的结构、数据类型、性能要求都不同,你得根据具体情况调整复用策略。
最后,记住一点:性能优化是个平衡的艺术。不是复用越多越好,也不是封装越细越好。得根据实际情况,找到那个平衡点。
更多推荐



所有评论(0)