当前位置: 首页 > news >正文

ArkTS List 的 cachedCount 我设到 20 反而掉帧——实测数据告诉你该设多少

你做的 App 要是也有长列表,肯定见过这个建议:“cachedCount设大点,滑动更流畅”。我一开始也信,直接拉满。我做的雷达鸭鸿蒙版首页就是个瀑布流,列表项高度不固定、图片多,结果把cachedCount从默认 1 改到 20 之后,用户反馈说"越滑越卡"。我愣了:这参数不是优化性能的吗?

直接上结论:在我的这个页面里,cachedCount=4cachedCount=20平均帧率高出 16fps,首屏时间还少了 340ms。下面我把测试过程和数据摊开讲。


测试页面长这样

一个List里塞 200 条 item,每个 item 包含一张网络图片、两行文字、一个关注按钮。图片高度不固定,模拟真实内容流。测试设备是 Mate 60 Pro,HarmonyOS NEXT 5.0,DevEco Studio 5.0 Release。

这个页面在雷达鸭鸿蒙版上对应的场景是"案例列表",一行一个卡片,滚动时图片懒加载。加载的是远端 CDN 图片,本地 mock 数据,避免网络波动影响测试结果。

核心代码:

// ListPerformancePage.etsimport{http}from'@kit.NetworkKit';interfaceCaseItem{id:string;title:string;cover:string;author:string;liked:boolean;}@Entry@Componentstruct ListPerformancePage{@StatecaseList:CaseItem[]=[];@StatecachedCount:number=1;// 测试时会改成 1/4/8/12/20aboutToAppear(){this.loadCases();}loadCases(){// 实际测试用本地 mock 数据,避免网络波动constmock:CaseItem[]=[];for(leti=0;i<200;i++){mock.push({id:`case_${i}`,title:`案例标题${i}`,cover:`https://example.com/cover/${i}.jpg`,author:`作者${i%30}`,liked:false});}this.caseList=mock;}build(){Column(){List({space:12}){LazyForEach(this.caseList,(item:CaseItem,index:number)=>{ListItem(){CaseCard({item:item})}.height(LengthMetrics.vp(this.estimateHeight(item)))},(item:CaseItem)=>item.id)}.cachedCount(this.cachedCount).scrollBar(BarState.Off).edgeEffect(EdgeEffect.Spring).layoutWeight(1)}.width('100%').height('100%').padding(16)}estimateHeight(item:CaseItem):number{// 简单预估:标题两行约 56,图片按 3:4 比例约 160,作者行 24,padding 24return264;}}@Componentstruct CaseCard{@Propitem:CaseItem;build(){Column(){Image(this.item.cover).width('100%').height(160).objectFit(ImageFit.Cover).borderRadius(8)Text(this.item.title).fontSize(16).fontWeight(FontWeight.Medium).maxLines(2).textOverflow({overflow:TextOverflow.Ellipsis}).margin({top:8,bottom:4})Row(){Text(this.item.author).fontSize(12).fontColor('#666')Button(this.item.liked?'已关注':'关注').fontSize(12).height(28).backgroundColor(this.item.liked?'#e0e0e0':'#ff6600')}.width('100%').justifyContent(FlexAlign.SpaceBetween)}.width('100%').padding(12).backgroundColor('#fff').borderRadius(12).shadow({radius:6,color:'rgba(0,0,0,0.06)',offsetX:0,offsetY:2})}}

我用这个脚本抓的帧率

DevEco Studio 自带的 Profiler 能看帧率,但为了重复跑对比,我写了段内部测试代码,在List滑动时每隔 100ms 记录一次主线程耗时,然后算 fps。

这个脚本的关键点在于:不要直接依赖ListonScroll事件频率,因为onScroll触发的间隔并不完全等同于渲染帧率。更稳的做法是起一个setInterval定时器,每秒统计一次这段时间内markFrame()被调了多少次,然后除以秒数得到 fps。

// FrameTracker.etsexportclassFrameTracker{privatelastTime:number=0;privateframeCount:number=0;privatefpsSamples:number[]=[];privatetimer:number|null=null;start(){this.lastTime=Date.now();this.frameCount=0;this.fpsSamples=[];this.timer=setInterval(()=>{constnow=Date.now();constelapsed=now-this.lastTime;if(elapsed>0){constfps=(this.frameCount*1000)/elapsed;this.fpsSamples.push(Math.round(fps));}this.lastTime=now;this.frameCount=0;},1000);}markFrame(){this.frameCount++;}stop():number[]{if(this.timer){clearInterval(this.timer);this.timer=null;}returnthis.fpsSamples;}averageFps():number{if(this.fpsSamples.length===0)return0;returnMath.round(this.fpsSamples.reduce((a,b)=>a+b,0)/this.fpsSamples.length);}}

用法:在ListonScroll回调里每帧调一次frameTracker.markFrame(),滑动 5 秒后停止,取平均 fps。

// 在 build 的 List 上加上 onScrollList({space:12}){// ... LazyForEach}.onScroll(()=>{this.frameTracker.markFrame();})

数据直接摆出来

cachedCount平均帧率首屏可见时间峰值内存
151fps680ms142MB
458fps620ms148MB
854fps710ms161MB
1246fps820ms178MB
2042fps960ms201MB

测试方法:每个取值跑 5 次,取中位数。滑动速度保持统一,从顶部匀速滑到第 50 条。测试前清一次应用进程,保证每次起始状态一致。

你看,cachedCount=1其实不算最差,但滑动时白块明显,因为上下都没预渲染。cachedCount=4是拐点,帧率最高、首屏最快。超过 8 之后开始往下掉,20 的时候肉眼已经能感受到卡顿。

我第一反应是不信。cachedCount不是缓存更多节点吗?多缓存不应该更流畅?结果数据打脸。我专门又跑了三轮确认,还把ListonScroll回调去掉,只测纯滑动,数据还是这个趋势。


为什么大了反而卡

ArkTS 的ListLazyForEach时,只渲染视口内的节点。cachedCount控制的是视口上下额外创建多少个节点。注意:是创建,不是只保留数据。

每个ListItem里面都有ImageTextButton、阴影、圆角。节点一多,主线程在滑动过程中要同时做这几件事:

  1. 创建新的ListItem组件实例;
  2. 触发图片下载和异步解码;
  3. 计算动态高度(如果你的 item 高度不固定,还要走一次布局);
  4. 回收滑出屏幕的节点。

cachedCount=20意味着你还没滑到那,系统已经提前创建了 20 个完整 item。每个 item 里的图片都开始请求,解码线程一下子挤进来。这跟你预想的"流畅"完全不是一回事。

问题也出在这:ArkTS 的 UI 主线程和动画线程是分离的,但节点创建和布局依然走主线程。cachedCount太大,主线程 GC 和布局开销会先吃掉帧预算。反映在数据上就是帧率下降、首屏变长。

还有一个容易被忽略的点:图片解码是并发的,但 GPU 纹理上传和合成是在主线程打点的。预创建太多 item,会导致一次上传大量纹理,某一帧直接爆掉 16ms 的预算。你看到的"卡顿"往往不是持续掉帧,而是隔三岔五顿一下。


我的最终配置

List({space:12}){LazyForEach(this.caseList,(item:CaseItem,index:number)=>{ListItem(){CaseCard({item:item})}.height(LengthMetrics.vp(this.estimateHeight(item)))},(item:CaseItem)=>item.id)}.cachedCount(4)// 实测这个页面最优.scrollBar(BarState.Off).edgeEffect(EdgeEffect.Spring).layoutWeight(1)

如果你 item 结构简单,比如只有一行文字,可以试探到 6-8。如果 item 里图片多、组件重,建议从 2-4 开始测。别上来就拉满。

还有一个细节:.height()一定要给ListItem固定或预估高度。如果你不给高度,List必须等子组件布局完才能知道每个 item 占多少空间,这会让cachedCount的预创建效果大打折扣。我当时图省事,有一版没设高度,结果cachedCount=4cachedCount=12几乎没差别,排查了半天才发现是这个原因。


几个容易踩的误区

误区一:缓存越大,滑动越不卡。错。缓存的本质是用提前创建换取滑动流畅度,但创建本身也是成本。超过某个阈值后,创建成本会超过缓存收益。

误区二:所有页面都用同一个值。错。瀑布流和简单文字列表的最优值差很多。我每次遇到新页面都会跑一组对比数据,而不是抄一个"最佳实践"。

误区三:只看平均帧率。错。卡顿很多时候是瞬时掉帧。cachedCount=20平均帧率 42fps,看着还行,但滑动时经常会突然跌到 30fps 以下,体感就是"一顿一顿"。所以测的时候一定要看 fps 曲线的最小值和方差,不能只取平均。


顺带一提的副作用

cachedCount调大还会让内存涨。我的测试里从 1 到 20,峰值内存涨了 60MB。鸿蒙 NEXT 对后台应用内存管控本来就比 Android 严,你一个列表页面多占 60MB,系统杀后台的概率就高一截。这点在低端机型上更明显。

所以这件事说到底是取舍:你愿意用内存和首屏时间换滑动流畅度吗?数据告诉我,换过头就亏了。我现在的做法是每个列表页面先按默认 1 跑一遍,再按 4 跑一遍,挑表现好的那个上线。结构简单的页面偶尔 8 更好,但超过 8 的我基本没再设过。


你列表页面cachedCount现在设的是多少?有没有跟我一样试过拉满然后掉帧的?欢迎评论区贴数据,我也想看看别的页面是不是这个规律。


老三— 10+ 年软件开发经验,软件设计师,人工智能应用工程师。专注鸿蒙 ArkTS 北向开发与 Web 前端,同时折腾 AI 自动化。不定期在 CSDN 分享鸿蒙和 AI 方向的技术文章。

本文遵循 MIT 协议,转载请注明出处。

标签:鸿蒙、ArkTS、List、性能优化、cachedCount

http://www.jsqmd.com/news/1159007/

相关文章:

  • 村田超级电容 DLCAP 选型指南:3大系列(DXE/DXF/DXG)特性对比与车载/工业应用解析
  • STM32C542定时器输入捕获:精确频率测量配置与实践指南
  • Minitab 田口设计实战:5因子4水平正交表生成与信噪比分析(附SPSSAU对比)
  • Claude Code安装原理:CLI二进制本质与跨平台部署指南
  • 2026企业级AI知识库“幻觉”率行业调研与测算深度分析报告
  • 5分钟学习笔记(FreeRTOS)(七)
  • 逆向工程实战:日志插桩与乱码解析破解电商平台a_bogus加密参数
  • Python 子类 __init__ 方法 3 种初始化策略对比与 super() 最佳实践
  • 高精度ADC系统设计与噪声抑制实战
  • Python量化开发:如何给金融行情API写一个优雅的“多线程并发、限流重试”二次封装?
  • 2026年6月最新杭州格拉苏蒂官方售后维修服务网点地址与客服电话 - 亨得利官方服务中心
  • App开发陷阱大揭秘!快来看避免中招!
  • 我有个问题,anaconda管理的环境有些不同环境却要用同样的库,下两份不会占用空间吗
  • 外贸成交01 | 外贸做了五年还在比价格?问题不在价格,在你的成交路径 - 外贸圈集团
  • Pearcleaner:让Mac告别“数字幽灵“的终极清理方案
  • TS2007FC与PIC18F2585构建高性能音频系统全解析
  • VMware虚拟机中Slackware 15完整安装与配置指南
  • RN项目iOS运行,react-native-safe-area-context 的 SafeAreaProvider 在 iOS 上是异步测量安全区域的
  • 拥抱“半成品”思维:为什么你的技术产品不该追求完美?
  • CS5090E 双节锂电池充电电路设计:从 5V USB 到 8.4V 的 90% 效率升压方案
  • 电阻电容电感磁珠:4类无源器件在EMC设计中的5种典型应用与避坑指南
  • LangChain+LangGraph实战-1.Agent入门
  • ESP32-S3 OpenOCD 调试排错指南:5种常见连接失败与解决方案
  • 2026年天津滨海全屋定制哪家好?5家环保与收纳实力派专业推荐 - 本地品牌推荐
  • ADP5350与PIC18F86J16组合在电源管理系统中的应用
  • Android MediaSession 与 MediaController 通信原理解析:4个核心类与异步回调机制
  • SpringAI完整学习指南(四)
  • SQL性能突降与CPU飙升:系统性排查六步法与实战解决方案
  • 高效精准,赋能航天品质——轴距可调式轴向元器件双边无损成形钳
  • 给Agent装上“手脚”:LangChain Tools 定义与使用完全指南