把长列表从 ForEach 换成 LazyForEach,首屏白屏确实没了,但快速滑动的时候还是掉帧,Profiler 里 build 调用次数依然居高不下。LazyForEach 解决的是“不一次性建完”,但它不负责“建得快”和“建得少”。真正流畅的长列表需要四件事同时到位:稳定的 Key、组件复用、合理的 cachedCount、以及状态作用域下沉。少任何一件,帧率就在某个环节卡住。

Key 用 index 就是给复用埋雷

LazyForEach 的 key 生成器如果返回 index,动态插入或删除一项之后,后面所有 item 的 key 全部偏移,框架认为“这些组件都变了”,复用池里的实例被错误地交给了错误的行。图片串项、选中状态错乱,根子都在这里。key 用 item.id,不是 index,也不要拼随机数。数据变更通知同样要精确:改一条用 onDataChange(index),新增用 onDataAdd(index),不要动辄 onDataReloaded() 全量刷新。

typescript

LazyForEach(this.dataSource, (item: Item) => {
  ListItem() {
    RowItem({ item: item })
  }
}, (item: Item) => item.id) // 稳定业务主键,不是 index

item.id 在数据生命周期内不变。如果业务上确实没有天然唯一字段,在数据入口生成并持久化一个,不要在渲染时临时拼。

@Reusable 的 aboutToReuse 不是可选回调

给列表项组件加 @Reusable 之后,滑出屏幕的实例进入复用池,滑入时走 aboutToReuse 而不是完整的 build。但复用池里的实例带着上一次的 @State,loading 态、展开态、动画进度如果不清,下一行数据就会“继承”上一行的临时状态。aboutToReuse 里必须做两件事:把新数据赋进去,把和行数据无关的临时状态复位。

typescript

@Reusable
@Component
struct RowItem {
  @State loading: boolean = false;
  @State item: Item = { id: '', title: '' };

  aboutToReuse(params: Record<string, Object>): void {
    this.item = params.item as Item;
    this.loading = false; // 临时状态必须手动清
  }

  build() { /* ... */ }
}

loading 不复位,复用后的行会带着一个永远转下去的圈。aboutToReuse 的参数是复用池传进来的新数据,用它覆盖旧状态,不要在里面重新发请求。

 

cachedCount 从一屏的一半开始试,不是越大越好

cachedCount 控制视口上下额外缓存的条目数。设得太小,快速滑动时来不及构建,露出白块;设得太大,内存和构建开销一起涨,帧率反而掉。官方建议从“一屏可见条目数的一半”开始试,一屏 8 条先设 4,然后用 Profiler 看帧耗时和内存峰值,往回调到“刚好不露白”的最小值。不要照搬某个固定数字,不同卡片高度、不同设备性能下最优值不一样。

状态下沉是最容易被忽略的那一件

列表数据放在父组件的 @State 里,任意一条的选中状态变化都会触发父组件 build,所有条目跟着重算。单条的展开、选中、点赞状态下沉到条目组件自己的 @State 或 @Local 里,父组件只负责“集合”。跨条目共享的高频数据用 AppStorage 局部隔离,不要用顶层 @Provide 一把罩下去。LazyForEach 管“建多少”,@Reusable 管“建多快”,cachedCount 管“预建多少”,状态下沉管“该不该重建”——四件事拆开看,帧率卡在哪一环就一目了然。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐