前言

在鸿蒙 6 的长列表场景里,很多问题表面上看是列表卡顿,实际上往往是数据源、状态管理、图片处理和容器选择一起出了问题。

不少开发者用了 LazyForEach 之后,依然会遇到几个典型现象。快速滑动时会短暂白屏,列表越滑越占内存,主线程偶发卡顿,列表项状态还会莫名其妙错乱。

问题并不在于 LazyForEach 不够强,而在于使用方式还停留在传统前端思路。ArkUI 的声明式渲染、组件复用和底层调度方式,和普通 Web 列表不是一回事。你如果继续按旧习惯写,性能问题很难真正解决。

一、普通数组为什么会拖垮惰性加载

很多人写长列表时,第一反应还是把普通数组直接传给视图层。代码看上去简单,但这往往正是性能问题的起点。

原因并不复杂。普通数组本身不具备细粒度的变更通知能力。你往列表里插入、删除或者更新某一项时,框架很难准确知道到底是哪个位置发生了变化。结果就是底层只能做更大范围的比对,严重时甚至会触发整段节点的重建。

在数据量小的时候,这种问题不一定明显。但一旦列表变长,或者伴随分页、刷新和局部更新,性能损耗就会集中暴露出来。

在 ArkUI 的惰性加载体系里,正确做法不是直接依赖普通数组,而是实现 IDataSource 接口。这个接口的价值,不只是为了符合框架规范,更重要的是它能给框架提供明确的数据变更信号。

当你在数据层主动告诉框架,某一项新增了、某一项删除了、某一项更新了,框架就能直接对对应索引的节点做精确处理,而不是盲目刷新整段列表。

实际工程里,更推荐把 IDataSource 封装成一个基础类。这样业务层只关心数据本身,不需要在每个列表里重复写通知逻辑,也更不容易漏掉关键调用。

下面这个写法就是一个典型例子:

public pushData(data: DataItem) {
  this.dataArray.push(data);
  this.notifyDataAdd(this.dataArray.length - 1);
}

这里真正关键的不是 push,而是 notifyDataAdd。没有这一步,框架就拿不到精确的变更位置。拿不到位置,后面的惰性渲染和节点更新就无法真正高效起来。

说得更直接一点,LazyForEach 不是自动优化器。它要发挥作用,前提是你得先把数据源接对。

二、组件会复用,状态不能乱放

很多列表问题不是卡,而是乱。

最常见的情况是,某一项明明没展开,却显示成展开状态;某一项明明没选中,却继承了上一项的样式;滚动一段距离后,列表项展示和真实数据对不上。

这类问题的根源,通常都和组件复用有关。

LazyForEach 提升性能的重要手段之一,就是复用已经离开可视区的组件实例。一个列表项滑出屏幕后,它不一定马上被销毁。系统更可能把它临时放进复用池。等新的数据进入屏幕时,再把这个实例拿出来继续使用。

这样做的好处很明显,能够减少频繁创建和销毁组件的开销。但它也带来了一个直接后果,那就是组件本身不再可靠地对应某一条固定数据。

也就是说,组件可以复用,内部状态就不能随便放。

如果你把 isExpandedisSelected 这类会直接影响界面展示的状态,写在组件内部,那么组件一旦被复用,就很容易出现两类错误。一类是旧状态被错误保留给新数据,另一类是新数据渲染时状态被错误重置。

这就是为什么很多列表看起来能跑,但一滑就乱。

真正有效的解决方式不是继续打补丁,而是改结构。要把状态从组件内部移出去,下沉到业务数据模型中。

更稳妥的做法,是用 Observed 修饰数据模型,再用 ObjectLink 把单条数据传入列表项组件。这样所有影响展示的状态都挂在数据对象本身,组件只是负责渲染。组件复用时,即使实例被重复利用,绑定的数据对象依然是当前那一条,界面自然就不会串。

这一点在长列表里尤其重要。因为列表越长,复用越频繁,状态放错位置的代价就越大。

这部分的核心结论其实很简单。组件可以复用,但状态不要跟着组件一起漂移。状态属于数据,就应该回到数据里。

三、图片优化不要硬搬前端缓存思路

做长列表时,图片往往是另一个高风险点。

很多前端开发者会有一个惯性。看到图片多,就想自己做一层缓存;看到滚动快,就想自己写懒加载;看到重复图片,就想在应用层维护位图字典。

这套思路在 Web 场景里有时成立,但在 ArkUI 这里,很多时候反而会带来更大的问题。

最典型的风险,就是在应用层长期持有图片对象引用。尤其是把解码后的位图对象放进字典、Map 或全局缓存里,这会直接干扰系统自己的回收节奏。结果不是更省内存,而是内存迟迟下不来,越滑越危险。

你需要明确一点。鸿蒙的原生图像组件本身就不是一个只负责显示的简单壳子。它背后已经接入了解码、调度、缓存淘汰等底层能力。

所以在长列表场景里,图片处理的原则不应该是自己接管更多,而应该是尽量少接管,把该交给系统的事情交给系统。

开发者真正要做的,通常只是提供稳定的图片源,配置合适的占位图,选择合适的缩放模式,同时避免在应用层长期持有大对象引用。

很多时候,下面这样的写法已经足够稳:

Image(this.data.imageUrl)
  .objectFit(ImageFit.Contain)
  .alt($r('app.media.placeholder'))

这个配置看起来很简单,但如果你的容器、数据源和状态管理都没有问题,它往往比手写缓存系统更可靠。

真正复杂的长列表优化,重点从来不是把系统已有的能力在应用层重复实现一遍,而是不要主动破坏系统原本的调度节奏。

四、容器选错了,惰性加载等于没开

很多人以为只要用了 LazyForEach,就已经进入惰性加载模式。实际上并不是这样。

真正决定惰性加载能不能生效的,还有容器。

如果你把它放进不支持按需渲染的普通滚动容器里,结果往往是外层容器初始化时,内部节点依旧会被一次性构建。这样一来,LazyForEach 的意义就会被大幅削弱,严重时甚至接近白用。

所以长列表一定要放在支持惰性渲染的专属容器中,比如 List 这样的原生列表容器。不要把长列表塞进结构不匹配的滚动布局里,再期待它自己变快。

另一个高频误区,是白屏出现后,开发者开始手动监听滚动偏移量,自己计算预加载时机、节点位置甚至显示范围。

这类方案看起来更主动,实际上成本很高。因为滚动事件本身就很频繁,你再叠加数学计算和状态判断,只会继续抢占主线程时间。

ArkUI 已经提供了更直接的能力。对于快速滑动时出现的短暂白屏,优先考虑容器原生提供的预加载参数,比如 cachedCount,往往更合适。

它的作用很好理解。系统会在当前可视区域上下,提前准备一部分即将进入屏幕的节点。这样用户快速滑动时,下一批内容更容易及时接上,白屏概率就会明显下降。

再结合 onScrollIndex 这类索引回调,分页加载逻辑也能顺手接进去,整体方案会比手写滚动计算更轻。

示例写法如下:

List({ space: 10 }) {
  // 此处省略内部组件排版与绑定逻辑
}
.cachedCount(5)
.onScrollIndex((firstIndex, lastIndex) => {
  // 在此处执行分页请求与数据拼接
})

这里需要额外提醒两点。第一,cachedCount 不能盲目调大。它确实能缓解白屏,但本质上是拿一部分内存换滚动体验,具体值必须结合列表项复杂度、图片数量和目标机型测试。第二,分页逻辑要尽量轻。回调里适合做阈值判断和触发请求,不适合塞大量同步计算。

这部分的本质结论很明确。惰性加载不是单个 API 的事情,而是容器、数据和调度一起配合的结果。容器没选对,后面很多优化都会失去意义。

五、不要只看时间戳,要看真正的上屏链路

很多性能分析之所以得不出结论,不是因为没有测,而是因为测错了。

有些开发者会在组件开始时记一个时间,在某个生命周期结束时再记一个时间,然后两者相减,想得到所谓的渲染耗时。这种方法在简单逻辑里可以参考,但对声明式 UI 来说,误差通常很大。

原因在于,ArkUI 的渲染不是单线程同步完成的。

一个组件从数据变化到真正显示到屏幕上,中间通常要经过多个阶段。逻辑侧先完成状态更新,再构建描述树,然后跨线程提交,接着由渲染线程完成布局与绘制,最后才是真正上屏。

你自己在业务代码里打的时间戳,通常只能覆盖其中一小段。它能说明这段代码跑了多久,但说明不了用户什么时候真的看到了变化。

如果你想追真正影响体验的耗时,就不能只靠手动打印时间差,而要使用系统级性能跟踪工具。

在鸿蒙体系里,hiTraceMeter 这类能力更适合做这件事。它的价值不在于写法复杂,而在于能把逻辑链路和系统链路串起来看。

例如下面这种埋点方式:

import hiTraceMeter from '@ohos.hiTraceMeter';

aboutToAppear() {
  hiTraceMeter.startTrace('ListItemRender', 1);
}

当然,真实项目里不会只打一行埋点就结束。你需要结合列表渲染的关键节点,把数据准备、组件出现、分页请求、图片加载等关键阶段串起来看,再放到性能分析工具里对照时间线排查。

这样你看到的,就不是某一行代码执行了几毫秒,而是一条真实渲染链路里,到底哪一段最慢,哪一段最容易抖动,哪一段最值得优先优化。

性能优化到了后期,拼的不是感觉,而是定位能力。只有先看清链路,优化才不会变成盲目试错。

总结

鸿蒙生态下的长列表优化,表面上是在调列表性能,实际上是在重构开发习惯。

很多性能问题,并不是因为框架能力不够,而是因为写法还停留在传统前端思路。你如果继续依赖普通数组,把状态塞进组件内部,在应用层强行管理图片缓存,再用不合适的容器去承载长列表,那么 LazyForEach 很难真正发挥价值。

更稳妥的方向其实很清楚。数据源要能感知变更,状态要从组件内部移到数据模型,图片调度尽量交给原生能力,长列表必须放进支持惰性渲染的专属容器,性能分析则要看完整上屏链路,而不是只看代码执行时间。

当你顺着 ArkUI 的底层逻辑来设计列表,很多问题会自然减少。内存会更稳,滑动会更顺,排查问题也会更有依据。

这类优化没有什么捷径。真正有效的做法,往往不是加更多手写逻辑,而是少做错误接管,把该交给框架的事情还给框架。

Logo

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

更多推荐