最近老菜和圣子将 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项目存在以下主要卡顿点:
🔴 严重性能瓶颈
-
updateWorldTransform (779.3ms, 19.8%) 问题:世界坐标变换更新占用最高。 原因:场景节点层级过深或节点数量过多。 优化建议:减少场景节点数量,包括合并静态物体、避免深层嵌套的节点结构、使用对象池复用节点、冻结不需要移动的节点变换。 -
Profiling overhead (557.6ms, 14.2%) 问题:性能分析本身的开销过大。 建议:这是 profiler 自身消耗,实际发布版本不会有。 -
dispatch (1,423.7ms, 36.2%) 问题:事件分发系统消耗巨大。 优化建议:减少事件监听器数量,及时移除不需要的事件监听、避免在每帧触发大量事件、使用事件委托模式。
🟡 次要性能问题
-
pyramidCulling (371.9ms, 9.5%) 问题:视锥剔除计算耗时。 优化建议:优化场景分块,使用更高效的遮挡剔除算法、减少需要剔除判断的对象数量。 -
(anonymous) 函数调用 (1,299.9ms, 33.1%) 问题:大量匿名函数调用。 优化建议:减少闭包和匿名函数使用、缓存函数引用、避免在循环中创建函数。
🟢 渲染相关优化点
-
_drawStarsIndicativeLines (195.0ms, 5.0%) -
_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(0, this._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 > 40000) return 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 都比较平稳。

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