一、ArkTS 的性能问题,往往不是“框架慢”,是“用法错”

从 Web 前端或 Android 迁移到鸿蒙 ArkTS 的团队,在业务逻辑跑通之后,通常会撞上三堵性能墙:长列表滚动卡顿、页面切换越来越慢、内存占用持续上涨直到 OOM。

这三类问题的根因各不相同,但有一个共性:它们都不是“ArkTS 框架本身慢”,而是开发者把前端的惯性写法带进了 ArkTS 的运行时模型。ArkUI 的声明式更新机制和 ArkCompiler 的 AOT 编译模型,对“状态作用域”和“组件创建/销毁”有明确的代价结构。用错方式,代价会被放大;用对方式,性能可以很稳。

这篇文章按“长列表 → 组件复用 → 内存泄漏”的顺序,拆解 ArkTS 高阶性能优化的核心手段和排查方法。

二、长列表:ForEach 一次性建完所有节点,是卡顿的第一因

2.1 为什么 ForEach 不适合长列表

ForEach 在组件挂载时会一次性创建全部子节点。一万条数据意味着首屏就要构建上万个 item 组件实例,UI 线程被长时间占用,结果是白屏一到两秒、滚动掉帧、内存随滚动持续上涨。

LazyForEach 配合实现了 IDataSource 的数据源,只构建当前视口加缓冲区内的组件,滚动时按需“上屏/下屏”,视口外的节点被销毁或回收。这是长列表的第一道优化。

2.2 数据源的通知必须是增量的

LazyForEach 的收益是条件收益:只有当数据变化时只通知变化的那部分,已经渲染的节点才不会被作废。如果分页追加时调用 notifyDataReload() 做全量重载,懒加载省下来的创建开销会在每次翻页时被原样还回去,表现为“平时丝滑、翻页必卡”。

正确的通知方式是增量操作:追加用 notifyDataAdd(index),更新单条用 notifyDataChange(index),删除用 notifyDataDelete(index)。让框架只处理真正变化的那一段,而不是整批重建。

typescript

class WaybillDataSource implements IDataSource {
  private data: Waybill[] = [];
  private listeners: DataChangeListener[] = [];

  public addItem(item: Waybill): void {
    this.data.push(item);
    // 只通知新增的这一条,不整批重载
    this.listeners.forEach(l => l.onDataAdd(this.data.length - 1));
  }

  public reloadAll(): void {
    // 只在数据源完全替换时使用,日常追加禁用
    this.listeners.forEach(l => l.onDataReloaded());
  }
}

2.3 Key 的稳定性决定节点是复用还是重建

Diff 算法通过 key 判断节点身份。用索引作为 key,删除第一项后所有后续项的索引都变了,框架认为它们全是新节点,销毁旧组件并重建——输入框里用户已输入的内容丢失,展开态重置。key 必须是稳定的业务唯一标识(如运单号),不是数组下标,也不是会变化的字段。

2.4 V2 体系下用 Repeat 替代 LazyForEach

在状态管理 V2 体系中,官方推荐使用 Repeat + .virtualScroll() 替代 LazyForEach。Repeat 自身默认具备组件复用能力,API 更简洁,不需要额外维护 IDataSource 实现和 @Reusable 装饰器。项目已经在 V2 体系下时,新列表页优先用 Repeat;存量 V1 列表可以在迁移到 V2 时一并替换。

三、组件复用:@Reusable 复用的是实例,不是状态

3.1 复用的收益与陷阱

@Reusable 让滑出屏幕的组件实例进入复用池,新数据进屏时直接复用,省去 build() 的重建成本。但复用的只是组件实例,状态变量不会自动回到初值,aboutToAppear 也不会再触发。

凡是“数据里没有、但 UI 上会变”的东西——展开态、选中态、动画进度、输入框内容——如果不显式处理,就会从上一张卡片“串”到下一张卡片上。

3.2 aboutToReuse 必须重置所有 UI 状态

正确做法是在 aboutToReuse 生命周期里,把所有 UI 状态重置为基于新数据的初始值:

typescript

@Reusable
@Component
struct WaybillCard {
  @State waybill: Waybill = new Waybill();
  @State expanded: boolean = false;      // 展开态
  @State selected: boolean = false;      // 选中态

  aboutToReuse(params: Record<string, Object>): void {
    this.waybill = params.waybill as Waybill;
    // 关键:必须重置所有 UI 状态,否则会从上一个卡片串过来
    this.expanded = false;
    this.selected = false;
  }
}

不要把复用理解成“白捡了一个组件” ,它是一份需要你手动清理的“二手状态”。

3.3 状态下沉:@Prop 的深拷贝代价

@Prop 对复杂对象执行深拷贝。如果子组件只展示数据、不修改,深拷贝就是纯开销。子组件不需要修改父状态时,优先用 @ObjectLink(持有引用,不深拷贝),配合 @Observed 对象。状态管理 V2 的 @ObservedV2 + @Trace 提供字段级的精确更新,适合嵌套对象的深度观察场景。

四、内存泄漏排查:三类高频泄漏点

ArkTS 运行在 ArkCompiler 上,有垃圾回收机制,但引用关系没有断开时,GC 不会回收。以下是三类高频泄漏点。

4.1 全局事件监听未注销

订阅了系统事件(网络变化、屏幕旋转、蓝牙状态)或 EventHub 事件,但没有在 aboutToDisappear 中取消订阅。页面销毁后,事件回调仍持有页面对象的引用,导致页面无法被回收。

排查方法:DevEco Studio 的 Profiler 中查看 Heap 快照,观察页面实例是否在 aboutToDisappear 后仍然存在。如果存在,检查该页面注册的所有监听是否都有对应的反注册。

修复模式:监听和反注册必须成对出现,且反注册放在 aboutToDisappear 中,不要在 onPageHide 中——onPageHide 只是页面不可见,页面实例仍然存活。

4.2 定时器与动画未清理

setInterval 或显式创建的动画对象,如果没有在页面销毁时清除,回调会持续执行,持有页面引用。

修复模式:在 aboutToDisappear 中清除所有定时器和动画。更稳妥的做法是把定时器封装在组件内部,组件销毁时自动清理,而不是散落在业务代码里手动管理。

4.3 长生命周期对象持有页面引用

单例的 Service 或全局缓存,如果在内部保存了页面的回调或 this 引用,页面销毁后无法回收。典型的场景是“把回调注册到全局的网络请求管理器”,但请求完成后没有移除回调。

排查方法:Heap 快照中对比页面销毁前后的对象引用链,找到谁在持有页面实例。Retainers 视图会显示从 GC Root 到该对象的完整引用路径。

修复模式:长生命周期对象不应该持有短生命周期的页面引用。如果必须回调,使用弱引用,或者在页面销毁时显式移除回调注册。

五、性能诊断的正确顺序

遇到卡顿或 OOM,不要先改代码。先用 DevEco Profiler 定位是 UI 线程、渲染线程还是计算导致的。

Frame 模板关注 Lost Frames 和 Hitch Time 泳道,区分是组件创建慢、布局计算重、还是渲染提交慢。必须用真机 + Debug 包,模拟器不采集 ArkTS Callstack 和 Snapshot 数据。

第一嫌疑往往不是渲染本身,而是主线程上执行了本应放进 TaskPool 的同步计算——大 JSON 解析、批量数据转换、加密。先用 ArkTS Callstack 确认丢帧区间内主线程在做什么,再决定是“改渲染”还是“搬计算”。把计算搬进 TaskPool 之后仍然卡,才需要检查 LazyForEach、@Reusable、key 稳定性和状态作用域。

布局性能的真正变量是节点数量,不是“平铺还是嵌套”。减少不必要的包装节点、把长列表的 item 做薄、把大对象拆分成多个小组件以缩小状态更新的波及范围,比纠结 Row 还是 Flex 更有效。

六、避坑指南

坑1:分页追加调用 notifyDataReload()。 全量重载让 LazyForEach 的懒加载收益归零。用增量通知 notifyDataAdd。

坑2:@Reusable 不复位 UI 状态。 展开态、选中态、输入内容从上一张卡片串到下一张。在 aboutToReuse 里显式重置。

坑3:key 用数组下标。 删除一项后后续所有节点被判定为新节点,销毁重建。key 必须是稳定的业务唯一标识。

坑4:事件监听在 onPageHide 中反注册。 onPageHide 时页面实例仍存活,反注册后再次显示需要重新注册,容易遗漏。反注册放 aboutToDisappear。

坑5:不定位就改代码。 卡顿可能是主线程同步计算导致的,改列表渲染没有意义。先用 Profiler 的 ArkTS Callstack 定位丢帧区间内主线程在做什么。

七、总结

ArkTS 高阶性能优化的核心可以概括为三句话:长列表的流畅度取决于“增量通知 + 组件复用 + 稳定 key + 状态下沉”四件事的配合,任何一环缺失都会让其他优化归零。@Reusable 复用的是实例不是状态,aboutToReuse 必须显式重置所有 UI 状态。内存泄漏的排查靠 Heap 快照的 Retainers 视图,三类高频泄漏点是全局监听未注销、定时器未清理、长生命周期对象持有页面引用。优化的顺序永远是先定位再改代码,Profiler 的 ArkTS Callstack 是判断“该改渲染还是该搬计算”的第一依据。

下一篇预告:终端性能优化解决了“App 不卡”,但数据链路的下一环是“数据怎么沉淀成可分析的资产”。明天聊物流业财数据中台——多维度利润报表与成本归因分析。

> 本文作者:深圳+Eyf•半字节科技▪技术产品团队
> 物流、快递、快运作业管理系统,让每一票,都清晰可控。从任务分配到签收回传,一站式管理每个作业环节。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐