Skip to content

Stage05|Performance ​

Lesson055|性能问题的四个维度:CPU、GPU、Memory、IO ​

Tags: #Creator2.x #Stage05 #Performance #CPU #GPU #Memory #IODifficulty: ⭐⭐⭐☆☆


真实问题:玩家说“商城很卡”,你第一步改什么 ​

错误回答通常是“减少节点”“压缩图片”或“上对象池”。但“卡”可能指:

text
打开时停顿 800 ms      → IO / 解码 / 创建峰值
滚动持续低帧率         → CPU / GPU
反复进出后越来越慢     → Memory / 泄漏 / GC
首次显示图片时抖一下   → 上传 / shader / 字形预热

没有时间线和指标,任何优化都只是猜。

一、本课目标 ​

性能优化不是看到“卡”就随意减少节点或 DrawCall,而是先回答:

这一帧究竟被哪一种资源限制住了?

本课建立四个维度:

text
CPU:脚本、引擎更新、布局和提交
GPU:顶点、像素、带宽和渲染状态
Memory:内存、显存、GC 和峰值
IO:资源读取、解码、网络和磁盘

二、为什么“卡”不是一个答案 ​

下面几种问题都可能被用户描述为卡顿:

表现可能的根因
持续低帧CPU 或 GPU 长期超预算
偶发尖峰GC、资源加载、着色器编译、同步等待
页面打开慢IO、反序列化、纹理解码、首次渲染
滑动掉帧Layout、节点更新、Fill Rate 或输入处理
内存越玩越高引用、缓存、监听器或资源释放问题

第一步永远是把现象变成指标:

text
何时发生?
哪台设备?
持续多久?
平均值还是尖峰?
CPU、GPU、内存和 IO 哪个同时异常?

三、CPU 瓶颈 ​

CPU 可能花在:

  • Component update 和 lateUpdate。
  • Node 遍历、Transform 和激活状态传播。
  • Widget、Layout、ScrollView 刷新。
  • Label、Animation、Particle 数据更新。
  • 事件分发、定时器和对象创建。
  • 组装渲染数据和提交 DrawCall。

CPU 瓶颈的典型特征是:

text
主线程帧时间很长
GPU 仍有空闲
降低分辨率后改善不明显

四、GPU 瓶颈 ​

GPU 可能花在:

  • 顶点数量。
  • Fragment Shader 复杂度。
  • Overdraw 和 Fill Rate。
  • 纹理带宽。
  • Mask、Stencil、Blend 和后处理。
  • 屏幕分辨率过高。

典型特征:

text
降低分辨率或关闭特效后明显改善
CPU 主线程并不满
复杂半透明 UI 区域最严重

五、Memory 瓶颈 ​

内存问题包括:

text
JavaScript Heap
Native Heap
Texture Memory
Audio / Spine / Particle 数据
缓存和临时对象

要区分:

  • 当前占用高但稳定。
  • 场景切换后无法下降。
  • 峰值超过平台限制导致崩溃。
  • GC 频繁导致帧时间尖峰。

“内存高”不一定等于“泄漏”,需要观察生命周期和趋势。


六、IO 瓶颈 ​

IO 不只指磁盘:

text
本地文件读取
网络下载
资源解压
图片 / 音频解码
Asset Bundle 加载
纹理上传前的数据准备

首屏慢常见于 IO 和 CPU 同时工作。资源压缩可能减少下载量,却增加解压时间;降低图片尺寸可能减少显存,却需要重新导入和上传。


七、不要用错优化方向 ​

text
CPU 瓶颈 → 只压缩图片
GPU 瓶颈 → 只删 update
内存泄漏 → 只降低纹理分辨率
加载慢 → 只减少节点数量

这些做法可能有效,但没有证据时属于猜测。正确流程是:

text
复现
→ 采样
→ 定位维度
→ 找最大成本
→ 修改
→ 重新采样

八、案例:商城首页打开卡顿 ​

可能链路:

text
加载大量图片
→ 解码
→ 创建 SpriteFrame
→ 创建列表 Item
→ Layout 重排
→ 首次上传纹理
→ 大量半透明 UI 绘制

应分别记录:

  • 资源下载和读取时间。
  • 反序列化和实例化时间。
  • 首次布局耗时。
  • 首帧 CPU/GPU 时间。
  • 图片内存和纹理显存峰值。

九、常见误区 ​

误区一:DrawCall 越低越好 ​

DrawCall 只是 GPU/CPU 提交成本的一部分,Overdraw、Shader 和纹理带宽仍可能成为瓶颈。

误区二:内存高就是泄漏 ​

缓存、预加载和平台保留都可能让占用暂时偏高,要看是否持续增长以及引用链。

误区三:删节点能解决所有性能问题 ​

如果瓶颈是 GPU Fill Rate、IO 或纹理显存,删空节点可能没有明显收益。

误区四:优化前后只比较 FPS ​

还要比较帧时间分布、内存峰值、加载时间和设备条件。


十、练习与答案 ​

练习 ​

  1. 如何区分 CPU 瓶颈和 GPU 瓶颈?
  2. 为什么场景切换慢可能同时属于 IO 和 CPU 问题?
  3. 内存高和内存泄漏有什么区别?
  4. 性能优化为什么必须记录设备和复现步骤?

参考答案 ​

  1. 观察主线程时间、GPU 时间、分辨率变化后的结果和 Profiler 数据。
  2. 资源读取/解码属于 IO,反序列化/创建/布局属于 CPU。
  3. 内存高可能稳定,泄漏通常表现为生命周期结束后仍持续增长或对象无法回收。
  4. 不同设备、分辨率和平台的瓶颈不同,没有环境就无法比较结论。

性能调查的标准闭环 ​

text
定义用户动作
→ 固定设备、版本与数据
→ 采集基线
→ 判断 CPU/GPU/Memory/IO 主嫌疑
→ 提出一个可证伪假设
→ 只改一个核心变量
→ 同条件复测
→ 检查视觉、功能和副作用
→ 记录结论

“可证伪”意味着实验结果可能推翻假设。例如:

假设滚动慢主要由 GPU Overdraw 导致;把全屏透明层关闭后,GPU Frame 应显著下降,而 CPU 基本不变。

如果结果不符合,就换方向,不为原猜测找借口。

Creator 2.4.x 编辑器实验:四类压力开关 ​

实验资源和节点通过 Creator 编辑器创建,不修改生成目录。内存与 IO 实验在测试场景中进行,不放进正式业务。

PerformanceDimensionLab.ts ​

ts
const { ccclass, property } = cc._decorator;

@ccclass
export default class PerformanceDimensionLab extends cc.Component {
    @property(cc.Node)
    overdrawLayer: cc.Node = null;

    private cpuEnabled = false;
    private retained: number[][] = [];

    toggleCpu() {
        this.cpuEnabled = !this.cpuEnabled;
    }

    toggleGpu() {
        this.overdrawLayer.active = !this.overdrawLayer.active;
    }

    allocateMemory() {
        for (let i = 0; i < 2000; i++) {
            this.retained.push(new Array(100).fill(i));
        }
        cc.log('[perf-lab] retained=', this.retained.length);
    }

    clearMemory() {
        this.retained.length = 0;
    }

    loadResource() {
        const begin = Date.now();
        cc.resources.load(
            'perf-lab/large-image',
            cc.SpriteFrame,
            (err) => {
                cc.log(
                    '[perf-lab] load ms=',
                    Date.now() - begin,
                    'error=', !!err
                );
            }
        );
    }

    update() {
        if (!this.cpuEnabled) {
            return;
        }
        let value = 0;
        for (let i = 0; i < 200000; i++) {
            value += Math.sqrt(i);
        }
        if (value < 0) {
            cc.log(value);
        }
    }
}

OverdrawLayer 使用多层全屏透明 Sprite。large-image 使用合法测试资源。

观察 ​

  1. CPU 开关主要改变脚本帧时间。
  2. Overdraw 开关在低端/高分辨率设备更可能改变 GPU。
  3. allocate 使 JS 堆增加,但 clear 后不保证进程内存立即归零。
  4. 首次 load 与第二次 load 可能因缓存呈现不同 IO/解码时序。

实验是分类器,不是模拟真实业务的最终结论。

四个维度如何互相影响 ​

text
IO 读取大图
→ CPU 解码
→ Memory 峰值
→ GPU 上传
→ 首帧卡顿

因此性能瓶颈不是永远单选题。应找当前用户症状的主导阶段,并记录次要风险。

真实项目事故:先压图后页面反而更慢 ​

团队为了包体把图片压得更小,但目标平台运行时仍解码为大纹理,且新格式解码成本更高。结果包体下降,打开峰值变慢,纹理内存基本不变。

正确验收至少包含:

  • 下载/包体;
  • 冷加载时间;
  • 解码与首帧;
  • Texture Memory;
  • 画质;
  • 目标平台兼容。

诊断矩阵 ​

现象优先证据常见误判
单帧尖峰帧时间时间线、加载/GC/创建日志只看平均 FPS
持续低帧CPU/GPU 对照、分辨率实验先清缓存
内存阶梯涨多轮稳定平台、对象/纹理计数一次关闭没降就叫泄漏
首次慢后续快冷热缓存、上传/字形/Shader认为网络一定慢

版本边界与练习答案 ​

Creator 2.4.x 内置 Profiler 提供总览,深入 CPU/GPU/Memory 仍需 Chrome DevTools、原生平台工具和目标设备。不同工具指标口径必须记录。

  1. “FPS 低”能否判断 CPU 或 GPU? 不能,需要 CPU/GPU 对照、降分辨率、禁用逻辑等实验。
  2. 为什么 Memory 也会造成卡顿? GC、分配器、换页、系统内存压力和资源重载都可能产生尖峰。
  3. 为什么 IO 优化要测冷缓存? 热缓存可能绕开磁盘/网络,掩盖首次用户体验。
  4. 一次只改一个变量有什么价值? 能把指标变化归因到具体修改,避免多个改动相互抵消。

十一、本课总结 ​

text
先判断瓶颈维度,再选择优化手段。
CPU、GPU、Memory、IO 可能同时出现,但必须找最大成本。
性能结论必须绑定设备、场景、指标和验证方式。

十二、下一课预告 ​

text
Lesson056|FPS、Frame Time、刷新率与帧预算