外观
Stage05|Performance
Lesson058|CPU 性能:一帧时间到底花在哪里?
Tags: #Creator2.x #Stage05 #CPU #Update #LayoutDifficulty: ⭐⭐⭐⭐☆
真实问题:CPU 占满时,删掉几个 update 为什么没用
Profiler 显示主线程一帧 28 ms。团队搜索所有 update 并删掉几个空函数,结果几乎不变。
CPU 一帧不只运行用户 update:
text
输入与事件
→ Scheduler / Tween / Animation
→ 用户脚本
→ Layout / Widget / Label
→ Transform 与渲染数据
→ 批次构建和图形提交
→ GC / 引擎维护必须找到最长区间和调用次数,而不是按函数名优化。
一、本课目标
CPU 卡顿通常不是某一行代码突然变慢,而是每帧工作累积超过预算。本课建立 CPU 帧分析方法。
二、一帧 CPU 工作的组成
text
输入和事件
→ Scheduler / Timer
→ Component update
→ Animation / Action
→ Transform / Layout
→ Node 遍历和状态传播
→ RenderData 组装
→ DrawCall 准备和提交不同版本和平台的具体顺序可能不同,但这些类别构成了主要排查范围。
三、CPU 的四种常见浪费
- 调用太多:大量组件、定时器、事件和空 update。
- 单次太重:复杂排序、布局、字符串处理或同步加载。
- 重复计算:同一帧反复刷新 Layout、Transform 或 Label。
- 分配太多:临时数组、对象、闭包导致 GC。
四、update 优化不等于删逻辑
错误方式:
ts
update() {
if (!this.visible) {
return;
}
this.refreshAllItems();
}如果组件仍然每帧被调度,判断本身也有成本。更好的方式可能是:
ts
onEnable() {
this.schedule(this.refreshVisibleItems, 0.2);
}
onDisable() {
this.unschedule(this.refreshVisibleItems);
}具体选择要看刷新频率和交互需求。
五、遍历和布局成本
一个 UI 操作可能触发:
text
修改子节点尺寸
→ 父 Layout 标记 dirty
→ 重新遍历子节点
→ 计算位置
→ Widget 再次修正
→ RenderData 更新如果在循环中逐项修改并立即读取布局结果,可能反复触发计算。应尽量批量修改,再在需要时统一刷新。
六、字符串和 Label 的 CPU 成本
ts
update() {
this.label.string = 'Score: ' + this.score;
}每帧拼接字符串不一定是大问题,但在大量 Label 或复杂文本中会产生:
- 字符串分配。
- 文本解析。
- 顶点/纹理更新。
- 布局重算。
可以只在 score 改变时刷新,而不是每帧赋相同值。
七、CPU 和 DrawCall 的关系
DrawCall 既可能影响 GPU,也会增加 CPU 提交成本:
text
渲染组件准备数据
→ 状态切换
→ 组织批次
→ 提交 DrawCall因此 DrawCall 优化不能只看 GPU。真正要判断的是:提交成本是否占用了 CPU 帧预算,GPU 是否同时成为瓶颈。
八、案例:列表刷新卡顿
ts
for (const item of items) {
item.node.active = true;
item.refresh(data);
item.layout();
}潜在问题:
- 每个 Item 触发布局。
- 每个 Item 更新 Label。
- 每项都执行事件和动画。
- 产生大量临时对象。
改进方向:
text
先准备数据
→ 只更新可见项
→ 批量修改属性
→ 避免循环内强制读取布局结果
→ 最后统一刷新九、常见误区
误区一:减少 Node 数量一定能解决 CPU 卡顿
如果主要成本是字符串、资源解码或某个重函数,删空节点收益有限。
误区二:函数调用次数多就一定要合并成一个巨型脚本
合并可能降低调度成本,却增加耦合和维护成本,应先测量。
误区三:只看单次函数耗时
总成本 = 单次耗时 × 调用次数,频繁的小函数也可能成为主要成本。
误区四:布局刷新越早越好
过早读取可能强制同步计算,导致重复刷新。
十、练习与答案
- CPU 帧成本通常包括哪些类别?
- 为什么循环中逐项修改并立即读取布局可能很慢?
- Label 字符串应该每帧更新吗?
- 如何判断一个小函数是否值得优化?
答案:
- 输入、调度、脚本、动画、布局、遍历、RenderData 和提交。
- 每次修改都可能触发布局或 Transform 计算,形成重复工作。
- 通常只在数据真正变化时更新。
- 看总耗时、调用次数、是否位于长帧中,而不是只看单次耗时。
CPU 成本的三个乘数
text
总成本 ≈ 单次成本 × 调用次数 × 更新频率例如单次只需 0.02 ms 的操作:
text
0.02 ms × 500 个 Item × 60 FPS
→ 每帧约 10 ms因此优化可以来自:
- 降低单次算法复杂度;
- 缩小参与对象数量;
- 从每帧改成事件驱动或低频更新。
Creator 2.4.x 实验:同一任务的三种调度
CpuCostProbe.ts
ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class CpuCostProbe extends cc.Component {
@property([cc.Label])
labels: cc.Label[] = [];
private mode = 0;
private elapsed = 0;
private dirty = true;
setMode(value: number) {
this.mode = value;
this.dirty = true;
}
markDirty() {
this.dirty = true;
}
update(dt: number) {
this.elapsed += dt;
if (this.mode === 1) {
this.refreshAll();
} else if (this.mode === 2 && this.dirty) {
this.dirty = false;
this.refreshAll();
}
}
private refreshAll() {
for (let i = 0; i < this.labels.length; i++) {
const next = String(i + Math.floor(this.elapsed));
if (this.labels[i].string !== next) {
this.labels[i].string = next;
}
}
}
}对照
- mode 0:不刷新。
- mode 1:每帧全量刷新。
- mode 2:数据变化时刷新。
- 将 Label 数从 20 增加到 200。
- 固定 Label 尺寸与启用 Layout 分别测试。
记录函数调用数、Self/Total Time、Layout/Label 子调用和 p99。
遍历为什么常是隐藏乘数
ts
for (const item of allItems) {
if (item.visible) {
item.refresh();
}
}即使只刷新可见项,仍每帧遍历全部。更好的数据结构可能直接维护可见集合或索引范围。
优化前问:
- 数据量上限;
- 每帧实际参与数;
- 是否可由事件维护集合;
- 是否重复搜索节点/组件;
- 是否在嵌套循环中做字符串和分配。
真实项目故障:列表刷新只要 3 ms,为什么打开卡 80 ms
调用栈显示 refreshList 自身循环 3 ms,但它创建 200 Item,引发:
text
onLoad/onEnable
→ Label 字形
→ Layout 多轮刷新
→ Widget 对齐
→ Transform/RenderDataTotal Time 远高于 Self Time。修复:
- 首屏只创建可见项;
- 批量设置数据后统一布局;
- 避免每加一个 Item 立即刷新 Layout;
- 使用对象池但完整 reset;
- 将工作跨帧分摊并给出 loading 状态。
CPU 故障诊断
JS 函数不高,但一帧仍长
看引擎阶段、Layout、渲染数据、GC 和原生调用;Total Time 可能归在子栈。
每隔几秒长帧
检查批量定时任务、存档、日志、网络消息聚合和 GC。用时间标记对齐。
优化后平均改善但 p99 不变
持续循环变快了,尖峰来源仍存在。分别优化稳定成本和长帧。
版本边界与练习答案
Web 与 Native 的 JS 引擎、JIT、JSB 和采样工具不同。Creator 2.4.x CPU 结论必须在目标平台复测。
- 为什么空 update 也不应大量存在? 调度与调用本身有成本,但删少量空函数不一定击中主要瓶颈。
- 事件驱动为何常优于每帧轮询? 只在数据变化时做工作,降低更新频率乘数。
- Object Pool 是否必然降低 CPU? 不一定,reset、监听清理、池查找和过大池都有成本;要比较创建/销毁尖峰。
- 为什么看调用次数? 很小的单次成本可被大量对象和高频率放大。
十一、本课总结
text
CPU 性能要同时看调用次数、单次成本、重复计算和内存分配。
Update、Layout、Label、对象创建和渲染提交都可能成为 CPU 瓶颈。十二、下一课预告
text
Lesson059|GPU 性能:DrawCall、Overdraw、带宽和 Fill Rate