AI Agent助力Cocos项目性能暴涨6倍!


最近老菜和圣子将 PC 端 3D 星图应用《Cosmos》 移植到移动端 Native 平台时,遭遇了性能灾难——帧率从 60FPS 暴跌至个位数,画面卡顿几乎无法使用。

面对近万个星体实例、数万条连线和复杂天文计算,移动端不堪重负。这就需要 AI 配合从多维度进行精准优化。

  • 渲染优化
  • CPU 计算
  • 内存管理等

老菜讲如何分享下使用 Agent 来帮助优化经验和系统性优化,最终实现了从 8FPS 到 60FPS 的性能飞跃。

三角形数从57万降至11万(-80.9%), GFX 纹理内存从305 MB 压缩到65 MB(-78.7%),且视觉效果基本不变。

优化前

优化后

Agent 创建

创建自己的 AI Agent,由多个 MD 文档组成,包括了 Agent 的 System Prompt 和 Agent 的优化案例。


你是一位精通 Cocos Creator 3.8 的高级性能优化专家。擅长使用各类 Profiler 工具进行性能分析和优化代码,重点关注:

## 核心优化目标
1. 减少函数调用开销 - 合并频繁调用的函数,减少函数调用层级
2. 降低运算复杂度 - 简化算法,减少不必要的计算
3. 最大化对象复用 - 使用对象池,避免频繁创建销毁对象

## 具体优化要求

### 1. 函数调用优化
- 将高频调用的小函数内联化
- 减少 update/lateUpdate/tick 中的函数调用
- 合并相似功能的函数
- 避免深层嵌套调用
- 缓存函数引用,避免重复查找

### 2. 运算优化
- 缓存计算结果,避免重复计算
- 将循环外可提取的运算提到外部
- 使用位运算代替数学运算(适当情况下)
- 减少三角函数、开方等高开销运算
- 使用查表法代替复杂计算
- 避免在循环中进行对象创建

### 3. 对象复用优化
- 实现对象池机制(NodePool、自定义对象池)
- 复用 Vec2、Vec3、Color 等临时对象
- 使用 `out` 参数避免创建新对象
- 预分配数组和对象
- 复用事件对象

### 4. 内存优化
- 及时释放不用的资源
- 避免内存泄漏(移除事件监听、清理定时器)
- 使用弱引用避免循环引用

## 输出要求
1. 提供优化后的完整代码
2. 标注关键优化点并说明优化原因
3. 给出性能提升预估(如减少X%函数调用、降低Y%运算量)
4. 如有必要,提供优化前后的对比说明

## Profiler 性能分析指南

### A. DevTools Performance Profiler 分析

#### 关键指标分析:
1. Main Thread(主线程)分析
   - 识别长任务(Long Tasks)> 50ms
   - 找出 Scripting(脚本执行)时间占比高的函数
   - 分析 Update/LateUpdate/tick 的调用频率和耗时

2. Call Tree分析
   - 按 Self Time 排序,找出最耗时的函数
   - 按 Total Time 排序,找出调用链中的瓶颈
   - 识别 Hot Functions(热点函数)

3. Bottom-tree分析
   - 查看哪些底层函数被频繁调用
   - 分析函数调用来源
   - 优化高频调用的叶子节点函数

通过优化案例(比较长,只列部分)和上下文管理,能让 Agent 找到更好的底层方法/API 来提升性能。

优化流程

通过引擎 Profiler/Chrome Profiler/第三方 Profiler 先定位热点函数,再重点优化和分析。

Agent 优化分析 Profiler

大部分的优化都是配合 Agent,这里就不赘述和放具体截图。

Agent 分析的结果:这个 Chrome Profiler 的数据分析,您的 Cocos 3.8 重度3D项目存在以下主要卡顿点:

🔴 严重性能瓶颈

  1. updateWorldTransform (779.3ms, 19.8%) 问题:世界坐标变换更新占用最高。
    原因:场景节点层级过深或节点数量过多。
    优化建议:减少场景节点数量,包括合并静态物体、避免深层嵌套的节点结构、使用对象池复用节点、冻结不需要移动的节点变换。
  2. Profiling overhead (557.6ms, 14.2%) 问题:性能分析本身的开销过大。
    建议:这是 profiler 自身消耗,实际发布版本不会有。
  3. dispatch (1,423.7ms, 36.2%) 问题:事件分发系统消耗巨大。
    优化建议:减少事件监听器数量,及时移除不需要的事件监听、避免在每帧触发大量事件、使用事件委托模式。

🟡 次要性能问题

  1. pyramidCulling (371.9ms, 9.5%) 问题:视锥剔除计算耗时。
    优化建议:优化场景分块,使用更高效的遮挡剔除算法、减少需要剔除判断的对象数量。
  2. (anonymous) 函数调用 (1,299.9ms, 33.1%) 问题:大量匿名函数调用。
    优化建议:减少闭包和匿名函数使用、缓存函数引用、避免在循环中创建函数。

🟢 渲染相关优化点

  1. _drawStarsIndicativeLines (195.0ms, 5.0%)
  2. _syncActivating (869.4ms, 22.1%) 问题: 自定义渲染和同步逻辑耗时。
    优化建议: 使用批处理减少 Draw Call、优化顶点数据结构、避免每帧重建几何体。

开始优化

整个项目使用了自定义的 TimeSystem 驱动逻辑更新。

为了方便 Profiler 精准定位性能瓶颈,在优化前对几个关键脚本的 tick 函数都做了命名标记,确保在性能分析工具中能清晰区分各模块的 CPU 耗时。

更新节流和时间分区

游戏内存在数千的星球,需要每帧根据他们的星系和当前观察者为之决定这些星球是否渲染,和他们的 Label 透明度。

这里做了2个机制:

节流

  • 通过分档位控制不常用方法的更新频率,避免每帧都执行。
  • 提供多个更新频率档位(1帧/3帧/5帧/7帧/10帧),可根据需求动态配置。
  • 可以减少30-40%的函数调用开销。
export const UPDATE_INTERVALS = {
    SCALE:1,      // 实时更新:每帧, FPS = 60 时候x2
    SUPER_HIGH:1// 超高频更新:每1帧,FPS = 60 时候x2 2帧一次
    HIGH: 3,      // 高频更新:每3帧
    MEDIUM: 5,    // 中频更新:每5帧
    LOW: 7,       // 低频更新:每7帧
    SUPER_LOW: 10// 低频更新:每10帧


protected starOnTick(dt?: time.sec, tick?: num.int): void {
    if (this._placeholder) return;
    this._frameCounter++;
    if (this._frameCounter >=  UPDATE_INTERVALS.SUPER_LOW) {
        this._frameCounter = 0;
        this._syncActivating();
        this._syncInstancing();
        this._syncNameLabel();
    }
}

时间分片(Time Slicing)

  • 虽然近万个实例做了分帧加载,但仍集中在少数几帧内执行。
  • 通过随机初始化每个实例的帧计数器,将实例的更新操作分散到不同帧。
  • 避免大量实例在同一帧执行 update/tick 方法造成性能尖峰。
private _frameCounter =Math.round(UPDATE_INTERVALS.SUPER_LOW*Math.random());

优化节点RTS运算

从 updateWorldTransform 结果来看,场景里数千个星球参与渲染,虽然节点有做大量的 instancing,但是整体的 RTS 开销还是很大的。

由于整个宇宙比较大,每个太空星系都是个相对的节点管理器,我在每个星系里 scalar 做了 DirtyFlag 脏标记检查,避免重复触发 RTS。

  private _updateScale(exp: number): void {
        const scale = this._handleSpecialScale(10 ** exp);
        // DirtyFlag 检查:只对比一个值即可(统一缩放)
        if (this._lastScale !== scale) {
            this.node.setWorldScale(scale, scale, scale);
            this._lastScale = scale;
        }
    }

简化函数计算

原始代码内使用了大量 setRotationFromEuler 之类代码,但是只运算一个元素,我们可以直接在四元素内只修改部分数据,降低数据开销。

update(dt: number) {
    // setRotationFromEuler 内部会进行大量无关计算
    // 即使只修改 Y 轴,也会处理 X、Z 轴的计算
    this.node.setRotationFromEuler(0this._rotateAngle, 0);
}

// 引擎原始实现
setRotationFromEuler(x: number, y: number, z: number) {
    // 问题1: 设置完整的欧拉角(即使 x=0, z=0)
    Vec3.set(this._euler, x, y, z);
    this._eulerDirty = false;
    
    // 问题2: 计算所有三个轴的旋转(大量三角函数调用)
    const halfX = x * halfToRad;
    const halfY = y * halfToRad;
    const halfZ = z * halfToRad;

    const sx = Math.sin(halfX);
    const cx = Math.cos(halfX);
    const sy = Math.sin(halfY);
    const cy = Math.cos(halfY);
    const sz = Math.sin(halfZ);
    const cz = Math.cos(halfZ);
    
    // 问题3: 复杂的四元数计算(涉及所有轴)
    this._lrot.x = sx * cy * cz + cx * sy * sz;
    this._lrot.y = cx * sy * cz - sx * cy * sz;
    this._lrot.z = cx * cy * sz + sx * sy * cz;
    this._lrot.w = cx * cy * cz - sx * sy * sz;
    
    // 问题4: 触发子节点更新
    this.invalidateChildren(TransformBit.ROTATION);
}
//简化版
export function setAngleFromY(node: Node, y: number{
    // 优化1: 直接设置欧拉角,只修改 Y 值
    Vec3.set(node['_euler'], 0, y, 0);
    node['_eulerDirty'] = false;
    // 优化2: 只计算 Y 轴旋转(减少 4 次三角函数调用)
    const lrot = node['_lrot'];
    const halfY = y * halfToRad;
    // 优化3: 简化的四元数计算(X=0, Z=0 时的特殊情况)
    // 当 x=0, z=0 时: sx=0, cx=1, sz=0, cz=1
    // 简化为: q = (0, sin(halfY), 0, cos(halfY))
    lrot.x = 0;
    lrot.y = sin(halfY);
    lrot.z = 0;
    lrot.w = cos(halfY);
    
    // 优化4: 保持原有的子节点更新逻辑
    node.invalidateChildren(TransformBit.ROTATION);
}

使用 Global Uniform 减少 Shader 更新

太阳系数百个星体都是用了自定义的光照系统,根据发光球体的位置来计算光照方向。

  #if CUSTOM_SOLAR_LIGHT
      data.L = -FSInput_worldPos.xyz 
       + solarParams.xyz;
      data.L = normalize(data.L);
  #else

在原来实现方法中,除去剔除后的节点,每帧也需要同步近百次太阳的位置。

 private _sycSolarPos(): void {
        this._sharedPasses.forEach((pass, idx) => pass.setUniform(this._sharedHandles[idx], this._sun.node.worldPosition));
    }
    

我在新管线可以定义一个全局的 solarParams uniform,只需求全局更新一次即可,在老版本也可以 hack 一些没有使用到的全局 uniform 来实现。

多个 Uniform 合并

原始代码中有大量单 float 的 uniform,如:

this._pass0.setUniform(this._handlePow10, pow10);
this._pass0.setUniform(this._handleRatio, this._ratio);
        

我们可以把这些 uniform 合并成一个vec4 或者更大的数组。

内联计算减少函数调用开销

通过将向量运算(如 Vec3.subtract、Vec3.dot、lengthSqr)展开为直接的数学计算,消除了热点路径中的函数调用开销和临时对象分配。

这种内联优化在高频执行的 update 循环中能显著降低 CPU 开销,同时利用提前距离检查(sqDis > 40000)实现快速剔除,进一步提升了剔除算法的执行效率。

关键改进点:

  • 避免大量工具函数的调用栈开销
  • 减少中间临时变量(如 _v3a)的内存分配
  • 采用平方距离比较避免昂贵的 sqrt 运算
     // const _forward = camera._forward;
    // const _pos = camera.node._pos;
    // const forward = camera.forwardDir;
    // const _pos = camera.node._pos;

    // Vec3.subtract(_v3a, target._pos, _pos);
    // if (Vec3.dot(_v3a, forward)
    // const dis = _v3a.length()
    // return false
    // const { right, up } = camera.node;
    // return dis >200 ? true : false;
    //优化后
    const fwd = camera.forwardDir;
    const cp = camera.node._pos;
    const tp = target._pos;

    const dx = tp.x - cp.x;
    const dy = tp.y - cp.y;
    const dz = tp.z - cp.z;

    // 先做廉价的距离检查
    const sqDis = dx * dx + dy * dy + dz * dz;
    if (sqDis > 40000return true;

    // 然后做点积检查
    if (dx * fwd.x + dy * fwd.y + dz * fwd.z return true;
    return false
    

自定义 Shader 减少编译时间

参考轻量化光照 Shader 实现,通过剔除非必需的 Shader Chunk 和条件分支(Switch/Define),显著降低着色器编译时间和变体数量。

  • 可以参考:https://github.com/iwae/LightingModel

仅保留项目实际使用的光照模型、特性开关和渲染路径,避免引擎默认 Shader 中大量冗余的通用功能模块,从而加快首次加载速度并减少运行时 Shader 编译卡顿。

改进点:

  • 结合项目场景, 优化 Shader 内 BRDF 计算
  • 移除未使用的光照计算模块(如多光源、阴影、反射、天空盒等)
  • 减少预处理宏定义和条件编译分支,降低排列组合产生的变体数

机型适配

通过设备 GPU 性能分级(如Mali-G52/Adreno 618等入门级、Mali-G78/Adreno 730等中高端),动态调整渲染参数以适配不同硬件能力。

低端设备采用更激进的剔除距离(如200000 lengthSqr)、降低目标帧率(30fps vs 60fps)、启用更激进降帧策略,确保游戏在各档设备上都能维持流畅体验,避免低端机卡顿和高端机性能浪费。

关键改进点:

  • 建立 GPU 设备白名单/黑名单映射表,启动时自动识别档位
  • 分级配置剔除距离、geo、粒子数量等渲染参数
  • 实现自适应降帧机制
public static SegmentProfiles = {
    // 低性能档位 - 移动端低端设备
    LOW: {
        CIRCLE: 16,          // 圆环段数 (降低 65%)
        ELLIPSE: 64,         // 椭圆段数 (降低 75%)
        CYLINDER: 8,         // 圆柱段数 (降低 50%)
        LINE: 4,             // 渐线段数 (降低 33%)
        ARC: 24,             // 圆弧段数 (降低 62%)
    },
    
    // 中等性能档位 - 移动端中端设备/PC集显
    MEDIUM: {
        CIRCLE: 24,          // 圆环段数 (降低 48%)
        ELLIPSE: 128,        // 椭圆段数 (降低 50%)
        CYLINDER: 12,        // 圆柱段数 (降低 25%)
        LINE: 5,             // 渐线段数 (降低 17%)
        ARC: 32,             // 圆弧段数 (降低 50%)
    },
    
    // 高性能档位 - PC独显/移动端旗舰
    HIGH: {
        CIRCLE: 32,          // 圆环段数 (降低 30%)
        ELLIPSE: 192,        // 椭圆段数 (降低 25%)
        CYLINDER: 16,        // 圆柱段数 (保持原值)
        LINE: 6,             // 渐线段数 (保持原值)
        ARC: 48,             // 圆弧段数 (降低 25%)
    },
    
    // 超高性能档位 - 编辑器/开发环境
    ULTRA: {
        CIRCLE: 46,          // 圆环段数 (原始值)
        ELLIPSE: 256,        // 椭圆段数 (原始值)
        CYLINDER: 16,        // 圆柱段数 (原始值)
        LINE: 6,             // 渐线段数 (原始值)
        ARC: 64,             // 圆弧段数 (原始值)
    }
};

// 当前激活的段数配置
publicstatic Segments = GizmoConfig.SegmentProfiles.ULTRA;

// 根据GPU档位动态设置
publicstatic setQualityLevel(level: 'LOW' | 'MEDIUM' | 'HIGH' | 'ULTRA') {
    this.Segments = this.SegmentProfiles[level];
    console.log(`[Gizmo] 切换至 ${level} 档位,圆环段数: ${this.Segments.CIRCLE}`);
}

如通过 GPU 分级(GPU 一般都和 CPU 关联),如在手机端使用中低档的线段数量,在低端机上绘制更简单的几何渲染器,并没有太影响视觉。

纹理压缩

针对移动端 GPU 内存限制,构建独立的移动端资源 Bundle。

将纹理分辨率从 PC 端的1024-2048 px 降档至256-1024 px,并启用 ARM 平台原生支持的 ASTC 纹理压缩格式(6×6/8×8档位),在保证视觉质量的前提下,显存占用降低70-80%。

配合模型减面优化,综合降低GPU带宽压力和渲染开销。

  • UI 纹理: ASTC 5×5
  • 图片纹理: ASTC 6×6

移动端纹理压缩后:

LOD策略

基于原有的 LOD 策略, 在基础上针对手机做了更激进的面数和 Shader 优化,再保证画质的情况下,只有近距离观察星体时候始终最高档。

引擎源码优化

通过 Profiler 定位到性能开销最大的 Component 后,将其源码和性能数据喂给 AI(推荐Claude 4.5),让 AI 分析瓶颈并给出优化建议。

但这种方式需要对引擎底层架构非常熟悉,大部分修改都不能直接用。比如 AI 优化完 GeometryRenderer 后,我自己又修改了很久才能跑起来。

测试

目前很多工具类似鸿蒙的 DevEco 也提供了 AI 测试功能,我们可以看到优化后整体的 CPU Usage 和 GPU Usage 都比较平稳。

写在结尾

感谢老菜喵的分享,更多干货文章在他的公众号中。


关注 Cocos 官方公众号,获取更多干货内容!