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

ArkTS List 的 cachedCount 我设到 20 反而掉帧——实测数据告诉你该设多少
你做的 App 要是也有长列表肯定见过这个建议“cachedCount设大点滑动更流畅”。我一开始也信直接拉满。我做的雷达鸭鸿蒙版首页就是个瀑布流列表项高度不固定、图片多结果把cachedCount从默认 1 改到 20 之后用户反馈说越滑越卡。我愣了这参数不是优化性能的吗直接上结论在我的这个页面里cachedCount4比cachedCount20平均帧率高出 16fps首屏时间还少了 340ms。下面我把测试过程和数据摊开讲。测试页面长这样一个List里塞 200 条 item每个 item 包含一张网络图片、两行文字、一个关注按钮。图片高度不固定模拟真实内容流。测试设备是 Mate 60 ProHarmonyOS NEXT 5.0DevEco Studio 5.0 Release。这个页面在雷达鸭鸿蒙版上对应的场景是案例列表一行一个卡片滚动时图片懒加载。加载的是远端 CDN 图片本地 mock 数据避免网络波动影响测试结果。核心代码// ListPerformancePage.etsimport{http}fromkit.NetworkKit;interfaceCaseItem{id:string;title:string;cover:string;author:string;liked:boolean;}EntryComponentstruct ListPerformancePage{StatecaseList:CaseItem[][];StatecachedCount:number1;// 测试时会改成 1/4/8/12/20aboutToAppear(){this.loadCases();}loadCases(){// 实际测试用本地 mock 数据避免网络波动constmock:CaseItem[][];for(leti0;i200;i){mock.push({id:case_${i},title:案例标题${i},cover:https://example.com/cover/${i}.jpg,author:作者${i%30},liked:false});}this.caseListmock;}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作者行 24padding 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。这个脚本的关键点在于不要直接依赖List的onScroll事件频率因为onScroll触发的间隔并不完全等同于渲染帧率。更稳的做法是起一个setInterval定时器每秒统计一次这段时间内markFrame()被调了多少次然后除以秒数得到 fps。// FrameTracker.etsexportclassFrameTracker{privatelastTime:number0;privateframeCount:number0;privatefpsSamples:number[][];privatetimer:number|nullnull;start(){this.lastTimeDate.now();this.frameCount0;this.fpsSamples[];this.timersetInterval((){constnowDate.now();constelapsednow-this.lastTime;if(elapsed0){constfps(this.frameCount*1000)/elapsed;this.fpsSamples.push(Math.round(fps));}this.lastTimenow;this.frameCount0;},1000);}markFrame(){this.frameCount;}stop():number[]{if(this.timer){clearInterval(this.timer);this.timernull;}returnthis.fpsSamples;}averageFps():number{if(this.fpsSamples.length0)return0;returnMath.round(this.fpsSamples.reduce((a,b)ab,0)/this.fpsSamples.length);}}用法在List的onScroll回调里每帧调一次frameTracker.markFrame()滑动 5 秒后停止取平均 fps。// 在 build 的 List 上加上 onScrollList({space:12}){// ... LazyForEach}.onScroll((){this.frameTracker.markFrame();})数据直接摆出来cachedCount平均帧率首屏可见时间峰值内存151fps680ms142MB458fps620ms148MB854fps710ms161MB1246fps820ms178MB2042fps960ms201MB测试方法每个取值跑 5 次取中位数。滑动速度保持统一从顶部匀速滑到第 50 条。测试前清一次应用进程保证每次起始状态一致。你看cachedCount1其实不算最差但滑动时白块明显因为上下都没预渲染。cachedCount4是拐点帧率最高、首屏最快。超过 8 之后开始往下掉20 的时候肉眼已经能感受到卡顿。我第一反应是不信。cachedCount不是缓存更多节点吗多缓存不应该更流畅结果数据打脸。我专门又跑了三轮确认还把List的onScroll回调去掉只测纯滑动数据还是这个趋势。为什么大了反而卡ArkTS 的List用LazyForEach时只渲染视口内的节点。cachedCount控制的是视口上下额外创建多少个节点。注意是创建不是只保留数据。每个ListItem里面都有Image、Text、Button、阴影、圆角。节点一多主线程在滑动过程中要同时做这几件事创建新的ListItem组件实例触发图片下载和异步解码计算动态高度如果你的 item 高度不固定还要走一次布局回收滑出屏幕的节点。cachedCount20意味着你还没滑到那系统已经提前创建了 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的预创建效果大打折扣。我当时图省事有一版没设高度结果cachedCount4和cachedCount12几乎没差别排查了半天才发现是这个原因。几个容易踩的误区误区一缓存越大滑动越不卡。错。缓存的本质是用提前创建换取滑动流畅度但创建本身也是成本。超过某个阈值后创建成本会超过缓存收益。误区二所有页面都用同一个值。错。瀑布流和简单文字列表的最优值差很多。我每次遇到新页面都会跑一组对比数据而不是抄一个最佳实践。误区三只看平均帧率。错。卡顿很多时候是瞬时掉帧。cachedCount20平均帧率 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