HarmonyOS Grid 滑动卡顿怎么排查:中式美食菜谱封面墙 cachedCount 怎么取值

Grid 做图片墙时,最容易被忽略的不是布局本身,而是滑动时“提前准备多少个 GridItem”。我在中式美食的菜谱封面墙里遇到过一个很典型的现象:缓存设得太保守,快速滑动时边缘会短暂空一下;缓存设得太激进,页面还没滑过去,图片解码和节点准备已经把主线程拖慢了。
所以 cachedCount 不能按感觉填,也不是越大越好。它更像一个缓存窗口:可视区附近准备一点,让快速滑动能接住;但不要把很远的图片也提前拉进来,不然页面卡顿只是从“滑动时卡”变成了“还没滑就先卡”。
先把几个职责说清楚
这几个词放在一起时,很容易混着用:
| 名称 | 负责什么 | 判断口径 |
|---|---|---|
| Grid | 负责网格布局,比如三列菜谱封面墙 | 先看列数、间距、单项高度是否稳定 |
| virtualScroll | 负责按需渲染,不一次性创建所有项 | 数据量大、图片多、滑动长时优先考虑 |
| cachedCount | 负责可视区外提前准备多少项 | 太小容易露白,太大容易多做解码和布局 |
| 图片兜底 | 负责图片还没回来或失败时页面不闪 | 占位图、失败图、加载态要提前设计 |
官方文档里 Grid 的 cachedCount 说明比较直接:设置缓存后,会在 Grid 显示区域上下缓存一定数量的 GridItem;LazyForEach 或开启 virtualScroll 的 Repeat 超出显示和缓存范围后会被回收。这个规则放到图片墙里,就能转成一个更容易判断的工程问题:我到底要为“下一段滑动”提前准备多少内容。
问题是怎么发生的
假设菜谱封面墙一次显示 3 列、4 行,也就是可视区里有 12 个封面。现在用户快速往下滑,页面需要马上接住下一屏内容。
如果 cachedCount 太小,Grid 只准备了很近的一点内容。用户手势一快,图片还没加载完,边缘就会露出空白占位。
如果 cachedCount 太大,Grid 会提前准备很多屏外内容。看起来很稳,但图片墙的问题是每个 GridItem 都带封面图,节点创建、图片解码、圆角裁剪、阴影这些成本会被一起提前触发。
我一般先用下面这个规则判断:
- 文字列表可以稍微大胆一点,因为单项成本低;
- 图片墙先保守一点,因为图片解码、缓存和裁剪成本高;
- 如果首屏加载已经慢,先别急着加 cachedCount,应该先查图片大小、占位和缓存策略;
- 如果只有快速滑动露白,才考虑适当增加 cachedCount。
案例一:缓存太小,快速滑动容易露白
先看一个容易出问题的写法:
Grid() {
Repeat(this.recipeCards)
.each((item: RecipeCard) => {
GridItem() {
RecipeCoverCard({ item })
}
})
.key((item: RecipeCard) => item.id)
.virtualScroll()
}
.columnsTemplate('1fr 1fr 1fr')
.cachedCount(1)
这个写法看起来没有明显错误。它确实启用了按需渲染,也给了缓存数量。但放到图片墙里,cachedCount(1) 可能只够接住很轻的滑动。只要图片较大,或者用户从首页快速滑到分类结果页,边缘就可能出现短暂空白。
我本地用一个简化模型算了一下:可视区 12 项,缓存 1 行时,准备范围是 18 项。也就是说,除了可视区,只多准备了 6 项。对于三列图片墙来说,这只是上下各一行,手势稍微快一点就会顶到边界。
case-a-fast-fling
visibleCount: 12
preparedCount: 18
extraCount: 6
decision: too-small-cache-risk
这种情况不应该只怪 Grid,也不能简单说“图片加载慢”。更准确的判断是:缓存窗口太窄,图片兜底也没有把视觉断层接住。
案例二:缓存太大,页面还没滑就先做了太多事
再看另一个极端:
Grid() {
Repeat(this.recipeCards)
.each((item: RecipeCard) => {
GridItem() {
RecipeCoverCard({ item })
}
})
.key((item: RecipeCard) => item.id)
.virtualScroll()
}
.columnsTemplate('1fr 1fr 1fr')
.cachedCount(6)
这个写法通常是从“不要露白”的角度出发,但它会把问题推到另一边。可视区仍然只有 12 项,缓存 6 行时,准备范围可能变成 48 项。多出来的 36 个 GridItem 如果都有封面图,就会提前触发大量图片相关工作。
case-b-heavy-images
visibleCount: 12
preparedCount: 48
extraCount: 36
decision: too-large-cache-risk
这种页面的表现通常不是露白,而是滑动前几屏突然变沉,或者从搜索结果切到图片墙时有明显停顿。这个时候继续加 cachedCount 只会更糟,应该反过来查三个点:
- 封面图是不是已经做了缩略图,不要拿详情页大图直接塞进 Grid;
- GridItem 里有没有太多跟图片无关的复杂布局;
- 图片失败或加载中是否有稳定占位,避免反复改变单项高度。
我会怎么取值
我的做法是先从 cachedCount(2) 或 cachedCount(3) 起步,再按页面表现调。
Grid() {
Repeat(this.recipeCards)
.each((item: RecipeCard) => {
GridItem() {
RecipeCoverCard({
item,
placeholder: $r('app.media.recipe_placeholder'),
failed: $r('app.media.recipe_failed')
})
}
})
.key((item: RecipeCard) => item.id)
.virtualScroll()
}
.columnsTemplate('1fr 1fr 1fr')
.rowsGap(12)
.columnsGap(12)
.cachedCount(2)
这个取值不是固定答案,但它有一个比较稳的出发点:可视区 12 项时,缓存 2 行大约准备 24 项,多准备 12 项。对三列图片墙来说,通常可以接住一次正常快速滑动,同时不会提前准备太多远处图片。
case-c-balanced
visibleCount: 12
preparedCount: 24
extraCount: 12
decision: balanced
如果设备性能较强、图片已经做了缩略图、封面卡片结构很轻,可以再往上调一点;如果列表里还有渐变、阴影、标签、收藏按钮、评分等复杂元素,就不要轻易把 cachedCount 拉太高。
RecipeCoverCard 也要配合
cachedCount 只能控制节点准备范围,不能替你解决图片本身的问题。图片卡片要尽量稳定,至少做到三件事:
@Component
struct RecipeCoverCard {
@Prop item: RecipeCard
@Prop placeholder: Resource
@Prop failed: Resource
build() {
Column() {
Image(this.item.cover || this.placeholder)
.width('100%')
.aspectRatio(1.2)
.objectFit(ImageFit.Cover)
.borderRadius(12)
.alt(this.failed)
Text(this.item.name)
.fontSize(14)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('100%')
}
}
这里重点不是代码有多复杂,而是职责边界:
- Grid 负责布局和可视区;
- cachedCount 负责附近节点准备;
- RecipeCoverCard 负责图片占位、失败兜底和单项高度稳定;
- 数据层负责给封面缩略图,不要让 UI 层直接背大图压力。
这几个边界分开以后,排查就清楚了。露白先看缓存窗口和占位,卡顿先看图片大小和单项结构,不要所有问题都往 Grid 身上堆。
怎么验证
我本地用一个小脚本模拟了三列 Grid 的缓存窗口,核心输入是总数、列数、可视行数、首个可见项和 cachedRows。
const cases = [
{ name: 'case-a-fast-fling', columns: 3, visibleRows: 4, firstVisibleIndex: 36, cachedRows: 1 },
{ name: 'case-b-heavy-images', columns: 3, visibleRows: 4, firstVisibleIndex: 36, cachedRows: 6 },
{ name: 'case-c-balanced', columns: 3, visibleRows: 4, firstVisibleIndex: 36, cachedRows: 2 },
]
验证结果是:
cachedRows = 1 -> 准备 18 项,风险是快速滑动露白
cachedRows = 6 -> 准备 48 项,风险是图片解码压力过早上来
cachedRows = 2 -> 准备 24 项,比较适合作为图片墙起步值
这个脚本不能替代真机性能测试,但它能帮我先把判断口径算清楚。真机上还要继续看两类现象:
- 快速滑动是否出现空白、错位、旧图闪一下;
- 首屏进入、搜索结果切换、分类切换时是否明显卡顿。
如果出现第一类问题,先加一点缓存或加强占位;如果出现第二类问题,先降缓存、压缩封面图、拆轻 GridItem。
最后沉淀成一条规则
Grid 图片墙不要只盯着 cachedCount 一个数。更稳的检查顺序是:
- 先确定 GridItem 高度稳定,不要图片回来后把行高撑变;
- 再确认图片使用缩略图,不要把大图直接丢进列表;
- 然后从
cachedCount(2)或cachedCount(3)起步; - 快速滑动露白就微调缓存或占位;
- 进入页面卡顿就先查图片解码和单项结构,不要继续加缓存。
对菜谱封面墙这种图片密集页面来说,cachedCount 的价值不是“越多越稳”,而是把可视区附近准备好,把远处的成本留到真正需要的时候再做。这样滑动体验和内存压力才比较容易平衡。
更多推荐


所有评论(0)