
前言在前两篇中我们用LazyForEach实现了按需渲染用Reusable实现了组件复用。但快速滑动长列表时你可能遇到过白块闪烁——新数据来不及创建组件用户看到空白区域一闪而过。HarmonyOS 提供了cachedCount属性让开发者可以设置列表项/网格项的预加载数量在滑动到达之前提前创建好组件彻底消除白块。本文以「猫猫大作战」排行榜列表为锚点深入讲解cachedCount在List、Grid、Swiper、WaterFlow四个容器中的使用方式以及如何结合Reusable和LazyForEach找到缓存数量与内存开销的最优平衡点。提示本系列不讲 ArkTS 基础语法与环境搭建假设你已跟完第 1–68 篇。本篇是阶段二第 69 篇列表性能优化三部曲的第三篇。一、什么是 cachedCount1.1 问题场景不设置cachedCount时LazyForEach的行为如下滚动方向 → ↓ ┌─────────────┐ │ 可见区 ① │ ← 已渲染 ├─────────────┤ │ 可见区 ② │ ← 已渲染 ├─────────────┤ │ 可见区 ③ │ ← 已渲染 ├─────────────┤ │ 空白 │ ← 未渲染手指划到此处才开始创建 → ⚠️ 白块 ├─────────────┤ │ 空白 │ ← 未渲染 └─────────────┘设置cachedCount(3)后滚动方向 → ↓ ┌─────────────┐ │ 可见区 ① │ ← 已渲染 ├─────────────┤ │ 可见区 ② │ ← 已渲染 ├─────────────┤ │ 可见区 ③ │ ← 已渲染 ├─────────────┤ │ 缓存区 ④ │ ← ⭐ 预加载手指到达时瞬显 ├─────────────┤ │ 缓存区 ⑤ │ ← ⭐ 预加载 ├─────────────┤ │ 缓存区 ⑥ │ ← ⭐ 预加载 └─────────────┘1.2 原理示意cachedCount会指示滚动容器在当前可视区域前后各额外创建 N 个不可见组件放入缓存池中等待用户滑动到达cachedCount(3) ┌─── 预加载上方 3 个超出释放 ←──┐ │ │ │ ┌──── 可视区 ────┐ │ │ │ [Item-1] │ │ │ │ [Item-2] │ │ │ │ [Item-3] │ │ │ └────────────────┘ │ │ ┌──── 缓存区 ────┐ │ │ │ [Item-4] ⭐ │ ←── 预加载 │ │ │ [Item-5] ⭐ │ │ │ │ [Item-6] ⭐ │ │ │ └────────────────┘ │ └───────────────────────────────────┘cachedCount只在LazyForEach中生效对ForEach无效因为 ForEach 已经全量加载了。二、各容器中的 cachedCount|cachedCount | List | Grid | Swiper | WaterFlow | |在可见区域前后各缓存的数量 | 在可见区域前后各缓存的数量Grid 还需要乘以列数 | 在可见区域前后各缓存的数量 | 在可见区域前后各缓存的数量 |2.1 List 中的 cachedCountList({ space: 8 }) { LazyForEach(this.records, (item: GameRecord) { ListItem() { RecordCard({ record: item }) // Reusable 组件 } }, (item: GameRecord) item.id.toString()) } .cachedCount(5) // 上下各预加载 5 个 ListItem .width(100%) .height(100%)当cachedCount(5)时List 会可视区 10 条 上方缓存 5 条 下方缓存 5 条 最多 20 个组件同时存在滑动出上/下缓存区的组件会被回收或进入Reusable复用池2.2 Grid 中的 cachedCountGrid 的缓存算法略有不同实际缓存数 cachedCount × 列数。Grid() { LazyForEach(this.catsGrid, (cat: CatItem) { GridItem() { CatCard({ cat: cat }) } }, (cat: CatItem) cat.id.toString()) } .columnsTemplate(1fr 1fr 1fr) // 3 列 .cachedCount(3) // 实际缓存 3 × 3 9 个 GridItemcachedCount 值3 列 Grid 实际缓存数说明13最少缓存低内存消耗但快速滑动可能有白块39推荐值较流畅且内存可控515极流畅但内存开销较大1030仅在低端机大列表快速滑动时使用2.3 Swiper 中的 cachedCountSwiper(this.swiperController) { LazyForEach(this.heroImages, (img: ImageData) { Image(img.url) .width(100%) .height(100%) }, (img: ImageData) img.id.toString()) } .cachedCount(2) // 前后各预加载 2 页 .loop(true) .autoPlay(false)Swiper 的cachedCount常用于短视频滑动场景——预加载前后视频的 AVPlayer 资源到 prepared 状态实现无缝切换。2.4 WaterFlow 中的 cachedCountWaterFlow() { LazyForEach(this.flowData, (item: MediaItem) { FlowItem() { VideoCard({ video: item }) } }, (item: MediaItem) item.id.toString()) } .columnsTemplate(1fr 1fr) .cachedCount(4) // 上下各缓存 4 行三、cachedCount 与 Reusable 的配合3.1 缓存池与复用池的关系设置cachedCount后组件一共经历三个状态┌─────────────┐ 创建 → │ 可视区节点 │ ← 实际显示在屏幕上 └──────┬──────┘ │ 滑出可视区 ↓ ┌─────────────┐ │ 缓存区节点 │ ← 被 cachedCount 保留不显示但存活 └──────┬──────┘ │ 超出缓存区 ↓ ┌─────────────┐ │ Reusable │ ← 进入复用池等待被复用 │ 复用池 │ └─────────────┘当ReusablecachedCount同时使用时组件滑出缓存区后不会销毁而是进入Reusable的复用池等下一个新组件需要创建时直接取出复用。3.2 内存占用计算组件总数 可视区数量 cachedCount × 2上下 实际占用内存 ≈ 组件总数 × 单组件内存 示例 - 可视区 10 条 - cachedCount 5 - 单组件 ≈ 50KB含文字、阴影、图片 组件总数 10 5×2 20 个 总内存 20 × 50KB 1MB ✅ 完全可以接受 极端情况Grid 大图 - 可视区 6 列 × 4 行 24 - cachedCount 5 → 实际 5×6 30 - 单组件 ≈ 200KB大图 总内存 (24 30×2) × 200KB 84 × 200KB 16.8MB ⚠️ 需要关注3.3 推荐配置表容器列表项复杂度cachedCount 推荐值单组件内存预期内存List纯文本简单5~10KB~0.2MBList图片中等3~100KB~1.6MBGrid3 列中等3~80KB~1.9MBGrid大图复杂2~300KB~3.6MBSwiper视频极复杂2~5MBAVPlayer~20MBWaterFlow中等4~100KB~2.4MB经验法则从cachedCount(3)起步快速滑动测试如出现白块则 1直到流畅为止。四、项目实战排行榜缓存策略调优4.1 场景说明「猫猫大作战」的排行榜列表列表项RecordCard包含排名、玩家名、得分、排名变化箭头单个组件约 50KB。需要在 3 秒内滚动 500 条记录时保持 60fps。4.2 基准测试Entry Component struct LeaderboardPage { private dataSource: LeaderboardDataSource new LeaderboardDataSource(10000); State cachedCountValue: number 3; // 可调节 State visibleItems: number 0; State totalNodes: number 0; build() { Column() { // 调试面板 Row() { Text(cachedCount: ${this.cachedCountValue}) Button(-).onClick(() { if (this.cachedCountValue 0) this.cachedCountValue--; }) Button().onClick(() { if (this.cachedCountValue 20) this.cachedCountValue; }) } .padding(8) .width(100%) // 排行榜列表 List({ space: 8 }) { LazyForEach(this.dataSource, (item: GameRecord) { ListItem() { RecordCard({ record: item }) } }, (item: GameRecord) item.id.toString()) } .cachedCount(this.cachedCountValue) // 可动态调节 .width(100%) .layoutWeight(1) .onScrollIndex((start, end) { this.visibleItems end - start 1; // 估算总节点数 可视 cachedCount×2 this.totalNodes this.visibleItems this.cachedCountValue * 2; }) } .height(100%) } }4.3 调优过程cachedCount白块帧率低端机内存结论0 频繁28fps3.2MB太差1 偶尔35fps3.5MB一般3 很少52fps4.1MB推荐5 无57fps5.0MB较优10 无58fps6.8MB过度推荐配置cachedCount(5)是“白块消除“与“内存控制“的最佳平衡点。4.4 动态缓存适配// 根据设备性能动态设置 cachedCount function getRecommendedCachedCount(): number { const memory Number(deviceInfo.deviceMemory); // MB const isLowEnd deviceInfo.deviceType phone memory 4000; if (isLowEnd) { return 2; // 低端机节省内存 } else if (memory 8000) { return 8; // 高端机极致流畅 } else { return 5; // 中端机平衡 } } List() .cachedCount(getRecommendedCachedCount())五、cachedCount 与其他缓存策略对比策略作用域机制数据源适用场景cachedCount容器级预创建组件LazyForEach消除滑动白块Reusable组件级复用滑出节点任意减少创建开销组件冻结组件级不可见时不刷新LazyForEach减少非可见区域更新onVisibleAreaChange组件级感知可见比例任意按需加载资源四者组合使用List({ space: 8 }) { LazyForEach(this.dataSource, (item: GameRecord) { ListItem() { RecordCard({ record: item }) .reuseId(item.rank 3 ? top3 : normal) // Reusable 复用 } }, (item: GameRecord) item.id.toString()) } .cachedCount(5) // 缓存 5 个六、cachedCount 与组件冻结从 API 17 开始ArkUI 支持组件冻结功能当组件处于非可视区域含 cachedCount 缓存区时状态变量的变化不会触发组件刷新进一步节省性能List({ space: 8 }) { LazyForEach(this.dataSource, (item: GameRecord) { ListItem() { RecordCard({ record: item }) } }, (item: GameRecord) item.id.toString()) } .cachedCount(5) // 组件冻结默认开启API 17无需额外配置冻结 cachedCount 的协同效果缓存区中的组件虽然存活但不会响应状态变化刷新 UICPU/GPU 零开销。七、常见踩坑7.1 坑一cachedCount 与 ForEach 不生效// 错误cachedCount 对 ForEach 无效 List { ForEach(this.records, (item) { ListItem() { Text(item.name) } }) } .cachedCount(5) // ❌ ForEach 下此属性无效解决搭配LazyForEach使用。7.2 坑二cachedCount 与 Reusable 的 aboutToReuse 不触发cachedCount预创建的组件不会触发aboutToReuse因为组件是全新创建而非从复用池取出Reusable Component struct RecordCard { aboutToReuse(params: Recordstring, Object) { console.info(从复用池取出); // 缓存区新建组件时不触发 } aboutToAppear() { console.info(全新创建); // 缓存区新建组件时触发 } }理解cachedCount预创建 新创建aboutToAppearReusable复用 取出旧节点aboutToReuse。7.3 坑三缓存区太大导致内存警告// 错误无上限的 cachedCount LazyForEach(this.dataSource, (item) { ListItem() { HeavyImageCard({ data: item }) } }) .cachedCount(50) // ❌ 50×2100 个重图组件 → 内存直接爆炸合理做法组件复杂度最大 cachedCount 建议纯文本/轻量20含小图 50KB10含大图 200KB5含视频/Canvas 等重型组件27.4 坑四Swiper 中 cachedCount 预创建资源重复消耗// ✅ Swiper 中 cachedCount 与资源缓存配合 Swiper() { LazyForEach(this.videoData, (item: VideoData) { VideoPlayerView({ src: item.url }) }, (item: VideoData) item.id.toString()) } .cachedCount(2)Swiper 的cachedCount(2)会前后各多创建 2 个VideoPlayerView实例每个实例都包含一个 AVPlayer。如果前后页的视频已经 prepared需要确保只有缓存的 AVPlayer进入 prepared 状态而不是全部同时播放。八、性能验证方法8.1 使用 SmartPerf 验证# 抓取帧率数据 hyperfunk --app-pid $(pidof com.catbattle) --frame-record -o /data/output # 查看关键指标 # - Frame Time: 平均 16ms (60fps) # - Jank Frames: 1% # - 无白块通过录屏回放验证8.2 使用 HiLog 验证缓存命中import { hilog } from kit.PerformanceAnalysisKit; const TAG CachedCountDemo; const DOMAIN 0xFF00; Reusable Component struct RecordCard { Prop record: GameRecord new GameRecord(); private createTime: number 0; aboutToAppear() { this.createTime performance.now(); const totalNodes performance.getTotalNodeCount?.() ?? 0; hilog.info(DOMAIN, TAG, 新建组件: ${this.record.id}, 节点数: ${totalNodes}); } aboutToReuse() { const now performance.now(); const reuseCost now - this.createTime; hilog.info(DOMAIN, TAG, 复用组件: ${this.record.id}, 复用耗时: ${reuseCost.toFixed(1)}ms 0.1ms); } aboutToDisappear() { hilog.info(DOMAIN, TAG, ️ 销毁组件: ${this.record.id}); } }运行后观察日志// cachedCount(5) Reusable 的典型日志 新建组件: 0, 节点数: 12 ← 首次创建 12 个组件可见区缓存 新建组件: 1, 节点数: 12 ... 复用组件: 11, 复用耗时: 0.02ms ← 后续全从复用池取 复用组件: 12, 复用耗时: 0.01ms ...九、最佳实践速查表场景cachedCountReusable说明消息列表纯文本5✅推荐排行榜文本小图5✅推荐商品网格3 列3✅缓存数 3×39视频 Swiper2✅大资源需谨慎图片瀑布流4✅WaterFlow低端机优化2✅内存优先长列表快速滑动8✅流畅优先十、总结cachedCount是LazyForEach的核心搭档通过在可视区前后预创建组件完美消除滑动白块。与Reusable搭配使用时缓存区组件滑出后进入复用池内存和创建效率两不误。核心要点cachedCount(N) 向上预加载 N 个 向下预加载 N 个只在LazyForEach中生效对 ForEach 无效Grid 的实际缓存数 cachedCount × 列数从3 起步调整白块消失即停重型组件视频/大图谨慎设大≤ 5与Reusable 组件冻结配合使用效果最佳下一篇预告第 70 篇将深入IDataSource— 自定义数据源的进阶用法包括分页加载、服务端同步、数据合并等高级话题。本篇也是阶段二的收官之作如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源List 组件 cachedCount 参考Grid 组件 cachedCount 参考Swiper 组件 cachedCount 参考LazyForEach 懒加载最佳实践组件复用最佳实践开源鸿蒙跨平台社区第 67 篇Reusable 组件复用第 68 篇LazyForEach 懒加载第 70 篇IDataSource 自定义数据源