长列表卡顿是鸿蒙应用里最常被提到、也最容易被误判的性能问题。
多数人的第一反应是「数据太多」,于是去做分页、做虚拟滚动、把一屏的条数砍下来。做完发现还是卡。因为问题往往不在数据量,在LazyForEach 的缓存参数配错了。
这篇讲清楚三件事:cachedCount 到底控制什么、默认值为什么是 1、以及不同场景该配多少。
LazyForEach 和 ForEach 的差别,不是「一个快一个慢」,是创建时机不同。
ForEach 会在列表渲染时,把数据源里所有条目一次性创建成组件节点。1000 条数据就是 1000 个节点,哪怕屏幕只显示 8 条。
LazyForEach 只创建当前可视区域 + 缓存区的节点。滚出去的要回收,滚进来的要新建。
所以它有两个动作:创建和回收。这两个动作都是有成本的——创建要分配内存、走一遍组件生命周期;回收要断开引用、等 GC。
列表卡,往往不是创建太多,是创建和回收太频繁。
cachedCount 这个名字容易误解成「缓存多少条数据」,其实它管的是可视区外,额外保留几个已经创建好的条目。
设成 2,意思是:屏幕内显示 8 条,上下各多留 2 条,一共 12 个节点常驻。滚动时,只要新进入视野的条目还在这 2 条的缓冲区里,就直接复用,不新建。
它的默认值是 1。
这个默认值对「一屏能显示十几条」的紧凑列表够用,因为滚动是连续的、每次只移动一点点。但对「一屏只显示 3~4 条」的大卡片列表就不够了——用户手指一划就是一个屏高,缓冲区那 1 条瞬间被越过,剩下全要新建。
我测过一个大卡片列表(每张卡片约 240 高,一屏 3.5 条),cachedCount 保持默认 1。
现象是:慢速滚动还算顺,一切到快速滑动就掉帧,滑完停住的一瞬间会顿一下。
用 Profiler 看帧耗时,发现两个特征:
滚动过程中,onAppear 和 onDisAppear 的调用次数远高于预期。 快速滑 10 屏,正常应该各触发三十来次(每屏 3.5 条),实测是 90 多次。同一张卡片被反复创建、销毁了好几轮。
停住那一下有个明显的尖峰。 手指抬起后,列表要重新布局、把最后几屏的卡片补齐,这批新建集中在几帧里完成,帧耗时冲到 40 毫秒以上,用户感知就是「顿一下」。
这跟「数据太多」没关系。数据就 200 条,问题在于缓冲区太小,导致同一条数据被反复重建。
把 cachedCount 从 1 改成 5,滚动确实顺了,但出现两个新问题。
第一个是首屏变慢。 首次进入列表时,要先创建「可视区 + 上下各 5 条」,一共十几个节点。卡片里有图片、有复杂布局的话,这批创建会拖慢首帧。
第二个是内存涨。 每个保留的节点都持有图片解码后的位图、文本排版结果。大卡片列表里,5 条缓存能吃掉几十兆。
所以 cachedCount 不是越大越好,它是个用内存和首屏换滚动流畅度的旋钮。
实操上有个简单公式:cachedCount ≈ 一屏能显示的条数 × 0.5~1。
但公式只是起点,实际值要靠数据试。判断方法:
看 onAppear 的次数是否合理。 快速滑动 N 屏,理想情况是触发 N × 一屏条数 次。如果远超这个数,说明缓存在被反复击穿,该往上调。
看停住那一帧的耗时。 手指抬起后的 2~3 帧如果明显高于滚动中的平均帧耗时,说明收尾时在集中补齐,也是缓存偏小。
看内存有没有超。 调大之后连续滚动两分钟,内存曲线应该是平的。如果持续上涨不回落,说明节点没被正常回收——这时候要查的是数据源实现,不是 cachedCount。
调参数之前,先确认这两处没写错,否则调多少都没用。
第一处:数据源的 getData 不要在里面做重活。
IDataSource 的 getData(index) 会被频繁调用,而且可能在一帧里被调用多次(布局、测量、绘制各一次)。如果在里面查数据库、做字符串拼接、算价格,这些计算会成倍放大。
正确做法是数据在进数据源之前就处理好,getData 只做一次数组取值。
第二处:keyGenerator 必须稳定且唯一。
LazyForEach 靠 key 判断「这条是不是已经存在」。如果 key 用 index,删除中间某条之后,后面所有条目的 key 都会变,框架会认为「全变了」,整片重建。
key 要用数据本身的唯一 ID。这一条错了,cachedCount 调到 20 也没用,因为框架根本认不出哪些能复用。
这块的坑,共同点是现象和原因隔得远。
第一处:以为卡是数据量的问题。 200 条数据,按常识不该卡。于是往「是不是渲染太重」「是不是图片太大」上找,找了半天,实际是缓存击穿导致重复创建。判断入口其实只有一个:看 onAppear 的调用次数。次数正常,才轮到怀疑渲染。
第二处:不知道默认值是多少,也不知道它意味着什么。 默认 1 这个数写在文档里,但文档不会告诉你「1 对什么场景够、对什么场景不够」。得知道它跟「一屏几条」相关,才知道自己该不该调。
第三处:调完不知道有没有效。 改完 cachedCount 感觉顺了,但这是主观感受。真要确认,得在同一段滚动路径上对比改前改后的帧耗时曲线和 onAppear 次数。没有对照组,改完只是「感觉好了」,下次遇到类似的还是靠猜。
这三处都不是知识盲区,是不知道往哪看。工具都有,Profiler 能抓帧、能看调用次数,但打开之后先看哪一项,这个顺序得有人带着走一遍。
如果你自己摸这块,卡在「知道慢但不知道从哪切」,可以参考誉天教育的鸿蒙课这种安排,进行多家对比,看到讲的顺序,是先讲工具怎么看数据、再讲怎么改代码——这个顺序比反过来有用,因为不知道量什么,改就是猜。
LazyForEach 的性能,八成的问题出在三个地方:cachedCount 太小、getData 太重、key 不稳定。
前两个是配置问题,改一行就好;第三个是设计问题,改起来要动数据源。
但三个都指向同一件事:LazyForEach 的收益来自「复用」,复用的前提是框架能认出「这条我刚才有过」。 认不出,它就退化成 ForEach,还多付了一层判断成本。
调缓存之前,先确认 key 是稳的。顺序反了,调多少轮都是白调。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。