Unity GPU 合批优化详解 Unity GPU 合批优化详解从 Draw Call 到 Static Batching、Dynamic Batching、GPU Instancing、SRP Batcher再到 Profiler 和 Frame Debugger 验证。这份文档的目标不是让你背概念而是让你能看懂 Unity 为什么卡、该用哪种优化、怎么证明优化真的有效。先说结论Unity 渲染优化里经常听到一句话减少 Draw Call。但真正准确的说法应该是减少 CPU 给 GPU 下命令的成本同时不要把 GPU、内存、显存、剔除效率搞坏。也就是说Draw Call 少不一定就快Draw Call 多也不一定就慢。你要先知道瓶颈在 CPU 还是 GPU然后选对方法。本文重点补充 3 个最容易被忽略的点底层原理CPU、Render Thread、GPU、Draw Call、Batch、SetPass 到底是什么关系。Unity 实操Static Batching、Dynamic Batching、GPU Instancing、SRP Batcher 分别适合什么怎么开什么时候不要用。性能验证怎么用 Stats、Profiler、Frame Debugger 判断合批有没有成功以及优化后是不是真的变快。1. 一帧画面是怎么出来的玩家看到一帧画面大概经历这条链路游戏逻辑 ↓ CPU 更新世界状态 ↓ CPU 准备渲染数据 ↓ Render Thread / 渲染系统组织绘制命令 ↓ CPU 把命令提交给图形 API / 驱动 ↓ GPU 执行绘制 ↓ 屏幕显示1.1 CPU 做什么CPU 更像游戏的“调度员”。它负责玩家输入。角色移动。AI。物理。动画状态。UI 逻辑。场景物体显隐。告诉 GPU 这一帧要画什么。比如你的 RPG 场景里有玩家 敌人 NPC 房子 地面 树 UI 小地图CPU 要先判断这些东西当前在哪里、是否可见、用什么材质、用什么 Shader然后再组织绘制命令。1.2 GPU 做什么GPU 更像“绘图工厂”。它负责顶点变换。光照计算。阴影。贴图采样。像素填充。后处理。最终输出画面。如果 GPU 很忙通常会看到GPU Usage 高 GPU Frame Time 高 降低分辨率以后 FPS 明显提升 关闭阴影/后处理以后 FPS 明显提升如果 CPU 很忙通常会看到CPU Main Thread 高 Render Thread 高 Batches 很多 SetPass Calls 很多 场景物体很多但 GPU 并不满2. Draw Call、Batch、SetPass 是什么2.1 Draw CallDraw Call 是CPU 让 GPU 绘制一次几何体的命令。比如画这棵树 画这个箱子 画这个敌人 画这个按钮每发一次绘制命令都有 CPU 端开销。开销包括检查渲染状态。切换 Mesh。切换 Material。切换 Shader。上传参数。设置贴图。组织命令。所以 Draw Call 多了CPU 会忙。2.2 BatchBatch 可以理解成Unity 最终提交给底层渲染的一批绘制任务。很多时候你会把 Batch 和 Draw Call 近似理解成同一类指标。Unity Game 窗口右上角Stats里常看到Batches: 1200 SetPass calls: 300 Tris: 1.5M Verts: 2.2M这里的Batches越多通常 CPU 渲染提交压力越大。但它不是唯一指标。例如Batches: 20 Tris: 20MDraw Call 很低但是三角面太多GPU 也可能很卡。2.3 SetPassSetPass 可以理解成切换一次 Shader Pass / 材质渲染状态。它通常比普通 Draw Call 更贵。比如物体 A 用 URP Lit 材质 物体 B 用 Unlit 材质 物体 C 用另外一个带透明的 ShaderUnity 可能要不断切换渲染状态。这种切换会让 CPU 和渲染线程变忙。所以优化时不要只看 Batches也要看SetPass Calls Render Thread Camera.Render DrawRenderers3. 为什么合批能优化性能合批的本质把多个能一起画的物体尽量变成更少的绘制提交。3.1 不合批假设场景里有 1000 个石头CPU: 画石头 1 CPU: 画石头 2 CPU: 画石头 3 ... CPU: 画石头 1000可能产生1000 Batches3.2 合批以后如果这些石头能合批CPU: 这一批石头一起画可能变成10 BatchesCPU 提交命令次数减少Render Thread 压力下降。3.3 一个快递例子不合批1000 个包裹 跑 1000 趟快递站合批1000 个包裹装进几个大箱子 跑几趟就送完但注意如果你为了合批把所有东西塞进一个超级大箱子搬不动了也会慢。对应 Unity 里就是Draw Call 降了 但是 Mesh 太大 显存变多 剔除变差 GPU 更忙所以优化不是盲目合批而是选合适的批。4. Unity 能不能合批看哪些条件Unity 合批时最关心这些东西4.1 MeshMesh 是模型数据。包括顶点 法线 切线 UV 颜色 骨骼权重 三角面索引不同合批方式对 Mesh 要求不同。例如GPU Instancing 通常要求 Mesh 相同。Static Batching 不要求 Mesh 完全相同但要求静态且材质兼容。Dynamic Batching 对 Mesh 顶点数量限制很严格。4.2 Material材质是合批最容易踩坑的地方。看起来一样不代表能合。比如Tree_Mat Tree_Mat可以尝试合批。但如果你复制了材质Tree_Mat_01 Tree_Mat_02哪怕两个材质参数完全一样Unity 也可能认为它们是两个不同材质导致不能合批。4.3 ShaderShader 不一样通常不能合。例如URP/Lit URP/Unlit这两个渲染流程不同不能随便合在一起。4.4 Texture贴图不一样也经常破坏合批。例如 100 个 UI 图标icon_1.png icon_2.png icon_3.png如果每张图都独立成材质Batch 会很多。更好的做法是把小图打进同一个 Atlas 用同一个 UI 材质4.5 Transform位置、旋转、缩放也会影响合批。尤其 Dynamic Batching。比如非统一缩放scale (1, 2, 1)可能导致动态合批失败。4.6 Lightmap静态场景常用 Lightmap。但如果物体使用不同 Lightmap或者 Lightmap 参数不同也可能被分成不同批次。例如房子 A 使用 Lightmap 0 房子 B 使用 Lightmap 1它们可能不能放进同一个批次。4.7 Render Queue 和透明排序透明物体很麻烦。因为透明物体通常需要按照距离从远到近排序。例如玻璃 水面 半透明特效这些东西不能像不透明物体那样随便重排顺序所以合批空间会小很多。5. Static Batching 静态合批5.1 一句话理解把不会动的场景物体提前整理到一起减少运行时绘制提交。适合房子 墙 地面 道路 石头 固定树木 固定栏杆 固定装饰物不适合玩家 敌人 NPC 会移动的平台 会旋转的机关 会动态隐藏显示很多次的物体5.2 Unity 怎么开启项目设置Edit → Project Settings → Player → Other Settings → Static Batching场景物体选中 GameObject → Inspector → Static 勾选常用是勾Batching Static如果你直接勾Static总开关可能还会包含Lightmap Static Navigation Static Occluder Static Occludee Static这些不是每个物体都应该全开。5.3 底层大概做了什么原来House Mesh Rock Mesh Fence Mesh Road Mesh静态合批后Unity 会把这些静态物体的顶点数据整理成更大的批次。它的核心思想是既然这些东西不会动 那就提前把它们的世界坐标算好 运行时少做事5.4 优点优点很直接减少 CPU 渲染提交压力 适合大场景固定物体 不需要每帧重新合并5.5 缺点最大的缺点是内存。假设你有 100 个路灯原本都共享同一个 MeshLamp Mesh x 1 100 个 Transform静态合批可能会把它们的顶点按世界空间复制整理。结果内存增加 显存增加 加载时间增加所以 Static Batching 不是无脑全开。5.6 RPG 场景例子你的MainCity这类城市场景可以这样分适合 Static Batching城墙 地砖 房屋主体 固定木箱 固定摊位 固定灯笼架 石阶 桥 栏杆不适合 Static Batching玩家 Player Enemy NPC 任务交互点高亮 可破坏木箱 会开关的门 会摇摆的旗帜 粒子特效5.7 常见错误错误 1把 Player 勾成 Static后果移动、光照、剔除、导航都可能出问题错误 2整个场景所有东西全 Static后果内存上涨 加载慢 动态物体异常错误 3材质乱七八糟每个房子一个材质后果勾了 Static但合批效果很差更好的做法同类建筑尽量共用材质 贴图尽量做 Atlas6. Dynamic Batching 动态合批6.1 一句话理解Unity 每帧在 CPU 上临时把一些小 Mesh 拼起来画。适合很小的低模道具 简单小物件 少量小特效模型不适合角色 复杂敌人 高模建筑 大场景物体 大量动态单位6.2 为什么它会消耗 CPUDynamic Batching 不是免费优化。它每帧要做检查哪些物体能合 把顶点从本地空间转换到世界空间 重新整理顶点数据 提交给 GPU如果模型很小这点 CPU 开销可能值得。如果模型稍微复杂一点就会变成为了减少 Draw Call CPU 反而更累6.3 顶点限制常见说法是Dynamic Batching 适合 300 顶点以下的小 Mesh更准确一点它和顶点属性数量有关顶点属性包括Position Normal UV Tangent Color属性越多能动态合批的顶点数越少。例如一个只有 Position UV 的小面片 可能容易合 一个带 Normal Tangent 多套 UV 的模型 顶点不多也可能失败6.4 例子可以考虑 Dynamic Batching金币80 顶点 小石子60 顶点 小草片40 顶点不建议玩家8000 顶点 敌人6000 顶点 建筑20000 顶点6.5 什么时候关掉也没问题如果你项目里URP SRP Batcher 大量中高模 大量角色 GPU Instancing 已经处理重复物体Dynamic Batching 的价值可能不高。甚至可能因为 CPU 每帧处理合批导致更慢。实际项目里要用 Profiler 测不要凭感觉。7. GPU Instancing7.1 一句话理解同一个 Mesh、同一个 Material 的大量物体让 GPU 自己复制很多份。适合大量同种树 大量草 大量石头 大量子弹 大量同款士兵 大量同款敌人不适合每个物体 Mesh 都不同 每个物体 Material 都不同 普通 SkinnedMeshRenderer 角色 需要复杂独立材质逻辑的单位7.2 传统方式1000 棵树CPU: 画树 1 CPU: 画树 2 CPU: 画树 3 ... CPU: 画树 10007.3 Instancing 方式CPU 只告诉 GPUMesh Tree Material Tree_Mat Instances [ position 1, position 2, position 3, ... ]GPU 自己在 Vertex Shader 里读取每个实例的位置、旋转、缩放然后画出来。7.4 Unity 怎么开启材质上选中 Material → Inspector → Enable GPU Instancing代码里material.enableInstancingtrue;7.5 不要用 renderer.material 乱改材质这是新手最常踩的坑。错误写法renderer.material.colorColor.red;renderer.material会在运行时实例化一份材质。原本100 个敌人共用 Enemy_Mat变成Enemy_Mat_Instance_1 Enemy_Mat_Instance_2 Enemy_Mat_Instance_3 ...结果材质不再相同 合批被破坏 内存也增加更推荐usingUnityEngine;publicsealedclassEnemyColorSetter:MonoBehaviour{privatestaticreadonlyintBaseColorIdShader.PropertyToID(_BaseColor);[SerializeField]privateRenderertargetRenderer;privateMaterialPropertyBlockpropertyBlock;privatevoidAwake(){propertyBlocknewMaterialPropertyBlock();}publicvoidSetColor(Colorcolor){targetRenderer.GetPropertyBlock(propertyBlock);propertyBlock.SetColor(BaseColorId,color);targetRenderer.SetPropertyBlock(propertyBlock);}}这样可以避免复制材质。注意MaterialPropertyBlock 对 GPU Instancing 很常用 但在某些 SRP Batcher 场景下可能让 SRP Batcher 失效 最终要用 Frame Debugger 验证7.6 DrawMeshInstanced 例子如果你想自己批量画一堆相同 Mesh可以用usingSystem.Collections.Generic;usingUnityEngine;publicsealedclassSimpleInstancedRocks:MonoBehaviour{[SerializeField]privateMeshrockMesh;[SerializeField]privateMaterialrockMaterial;[SerializeField]privateintcount500;[SerializeField]privateVector2areanewVector2(50f,50f);privatereadonlyListMatrix4x4matricesnewListMatrix4x4(1023);privatevoidStart(){rockMaterial.enableInstancingtrue;for(inti0;icount;i){floatxRandom.Range(-area.x,area.x);floatzRandom.Range(-area.y,area.y);Vector3positionnewVector3(x,0f,z);QuaternionrotationQuaternion.Euler(0f,Random.Range(0f,360f),0f);Vector3scaleVector3.one*Random.Range(0.8f,1.2f);matrices.Add(Matrix4x4.TRS(position,rotation,scale));}}privatevoidUpdate(){constintmaxInstanceCount1023;for(inti0;imatrices.Count;imaxInstanceCount){intbatchCountMathf.Min(maxInstanceCount,matrices.Count-i);Graphics.DrawMeshInstanced(rockMesh,0,rockMaterial,matrices.GetRange(i,batchCount));}}}上面这个例子好懂但GetRange每帧会产生 GC。更好的写法是提前准备数组usingUnityEngine;publicsealedclassNoGcInstancedRocks:MonoBehaviour{privateconstintMaxInstanceCount1023;[SerializeField]privateMeshrockMesh;[SerializeField]privateMaterialrockMaterial;[SerializeField]privateintcount500;[SerializeField]privateVector2areanewVector2(50f,50f);privateMatrix4x4[]matrices;privatevoidAwake(){countMathf.Min(count,MaxInstanceCount);matricesnewMatrix4x4[count];rockMaterial.enableInstancingtrue;for(inti0;icount;i){Vector3positionnewVector3(Random.Range(-area.x,area.x),0f,Random.Range(-area.y,area.y));QuaternionrotationQuaternion.Euler(0f,Random.Range(0f,360f),0f);Vector3scaleVector3.one*Random.Range(0.8f,1.2f);matrices[i]Matrix4x4.TRS(position,rotation,scale);}}privatevoidUpdate(){Graphics.DrawMeshInstanced(rockMesh,0,rockMaterial,matrices,count);}}实际大型项目里如果实例数量特别多会继续考虑DrawMeshInstancedIndirect GPU Culling ComputeBuffer DOTS / Entities Graphics但初学阶段先理解 GPU Instancing 就够了。7.7 RPG 项目例子假设你后面场景里有100 个同款小怪 300 个同款资源点 2000 株草 500 棵同款树选择建议同款草、树、石头 → GPU Instancing 敌人普通 SkinnedMeshRenderer → 先别强行 Instancing 敌人武器/掉落物等静态 Mesh → 可以考虑 Instancing角色类对象更复杂因为通常涉及骨骼动画 蒙皮 装备 材质变化 血条 状态特效不要一上来就为了 Instancing 把角色系统搞复杂。先保证敌人数量合理 Animator 不滥用 LOD 做好 Update 控制好 对象池做好8. SRP Batcher8.1 一句话理解在 URP/HDRP 里减少材质和 Shader 参数切换的 CPU 成本。它和 GPU Instancing 不是同一个东西。GPU Instancing 解决大量相同 Mesh 相同 Material 的绘制提交问题SRP Batcher 解决大量使用兼容 Shader 的不同物体切换材质参数时 CPU 太忙的问题8.2 适合什么适合URP 项目 大量不同 Mesh 大量不同 Material Shader 兼容 SRP Batcher 场景中物体较多例如 RPG 城市场景房子 A 房子 B 墙 石头 道路 木箱 摊位 灯笼这些东西 Mesh 不一样不能靠 GPU Instancing 全解决。但如果 Shader 兼容 SRP BatcherCPU 设置材质参数的成本会明显下降。8.3 怎么开启URP 项目一般在 URP Asset 中开启Project 窗口找到 UniversalRenderPipelineAsset → Inspector → Advanced → SRP Batcher不同 Unity 版本位置可能略有差异找不到就搜索SRP Batcher8.4 Shader 怎么兼容一般 Unity 官方 URP Lit、URP Unlit 都是兼容的。自定义 Shader 要注意材质属性要放在 UnityPerMaterial CBUFFER 里 不要乱写每材质常量缓冲如果 Shader 不兼容Frame Debugger 里可能会看到 SRP Batcher 没有生效。8.5 SRP Batcher 和 MaterialPropertyBlock很多项目会用 MaterialPropertyBlock 给不同物体设置颜色。比如敌人受击变红 NPC 头顶高亮 任务目标描边这种写法很方便但要注意MaterialPropertyBlock 可能让该 Renderer 走不了 SRP Batcher不是说不能用而是少量使用没问题 大量使用要验证如果你有 5000 个草要变色更适合考虑 GPU Instancing 的实例属性。如果只是玩家锁定的 1 个 NPC 高亮用 MaterialPropertyBlock 很正常。9. 四种合批方式对比技术解决什么适合不适合主要风险Static Batching静态物体减少绘制提交建筑、地面、固定道具会移动的物体内存、显存增加Dynamic Batching小 Mesh 每帧临时合并低顶点小物件角色、大模型、大量复杂物体CPU 额外开销GPU Instancing大量相同物体草、树、石头、同款道具Mesh/Material 各不相同的物体材质实例化会破坏SRP Batcher降低 SRP 材质切换成本URP/HDRP 大量物体Built-in 管线、不兼容 Shader自定义 Shader 不兼容9.1 选择口诀固定不动的大场景Static Batching大量重复的同款物体GPU InstancingURP 项目里很多不同物体SRP Batcher很小很简单的动态物体Dynamic BatchingUICanvas 拆分 图集 减少材质切换10. UI 合批也很重要很多人只看 3D 场景忽略 UI。但 Unity UI 也会产生 Batch。10.1 UI 为什么会打断合批UI 合批受这些影响不同 Canvas 不同材质 不同贴图 不同 Mask 不同字体材质 不同渲染顺序 频繁 Layout 重建10.2 常见 UI 错误错误 1所有 UI 都放一个巨大 Canvas后果聊天消息刷新一下 整个 Canvas 可能都要重建错误 2每个图标都是独立贴图后果UI Batches 增加 材质/贴图切换增加错误 3大量使用 Mask、Shadow、Outline后果额外绘制和材质变化10.3 RPG 项目 UI 拆分建议例如你的游戏里有MainUI GameUI ChatUI ToastUI SettingsUI可以按变化频率拆静态 HUD Canvas: 玩家头像 名字 固定按钮 动态 HUD Canvas: 血条 蓝条 小地图玩家点 弹窗 Canvas: 设置 聊天 登录 Toast Canvas: 提示飘字这样一个小地图点移动不会导致整个主界面跟着大重建。10.4 聊天界面例子聊天 UI 里常见问题消息一多 滚动列表卡 LayoutGroup 每帧重排 ContentSizeFitter 频繁计算 文字生成 Mesh优化建议消息 Item 用对象池 只刷新可见范围 减少 ContentSizeFitter 嵌套 文字高度缓存 新消息来了再刷新布局 不要每帧 ForceUpdateCanvasesCanvas.ForceUpdateCanvases()不是不能用但不要每帧用。它适合打开界面时需要立刻拿到布局尺寸 特殊一次性布局计算不适合Update 每帧调用 滚动时每帧调用 聊天刷新时无脑调用多次11. 怎么判断合批成功11.1 Game 窗口 Stats打开Game 窗口 → Stats重点看Batches SetPass calls Tris Verts例子优化前Batches: 1800 SetPass calls: 650 Tris: 1.2M优化后Batches: 350 SetPass calls: 120 Tris: 1.2M这说明 CPU 渲染提交压力可能下降了。但还要看 Profiler不要只看 Stats。11.2 Profiler 看 CPU 还是 GPU打开Window → Analysis → Profiler先看CPU Usage Rendering GPU Usage Memory UI如果 CPU Rendering 高重点看 Batches、SetPass、Render Thread如果 GPU 高重点看阴影、后处理、分辨率、Overdraw、三角面、贴图采样11.3 Frame Debugger打开Window → Analysis → Frame Debugger点击 Enable。你可以一条一条看 Unity 这一帧画了什么。没合批时可能看到Draw Mesh Tree Draw Mesh Tree Draw Mesh Tree Draw Mesh Tree合批成功可能看到Draw Mesh Tree x 100 Render Mesh (instanced) SRP BatchFrame Debugger 最重要的作用是告诉你为什么没合批。常见原因Different Material Different Shader Keywords Different Lightmap Different Render Queue Different Shadow Pass Object is not static Material property block11.4 正确测试流程不要一边改一边凭感觉。建议流程1. 记录优化前数据 2. 只改一个优化点 3. 进入同一个场景同一个视角 4. 记录优化后数据 5. 对比 CPU、GPU、Batches、SetPass、内存 6. 确认没有画面错误记录表可以这样写项目优化前优化后是否变好FPS4258是Batches1600420是SetPass500130是CPU Main Thread18ms11ms是Render Thread12ms5ms是GPU Time9ms9ms基本不变Memory1.2GB1.35GB变高需要接受或继续优化12. 实战案例 1MainCity 场景假设你的MainCity有房子 80 个 地砖 300 块 摊位 40 个 树 200 棵 灯笼 120 个 箱子 100 个 玩家 1 个 敌人 20 个 NPC 30 个12.1 错误做法每个房子一个材质 每个树一个材质 所有东西都勾 Static 玩家也 Static 敌人也 Static UI 全放一个 Canvas可能结果Batches 很高 内存也高 动态物体异常 UI 刷新卡12.2 推荐做法建筑共享材质 勾 Batching Static 考虑 Lightmap地面合并贴图或使用统一材质 静态 必要时分块避免一个超级大 Mesh树同款树多 → GPU Instancing 固定少量树 → Static Batching 也可以 风吹动画树 → 不要直接 Static玩家、敌人、NPC不 Static 使用对象池 Animator 控制数量 LOD 远距离降低更新频率UIGameUI、ChatUI、ToastUI 分 Canvas 小地图点只在数据变化时刷新 聊天列表用回收/对象池12.3 预期效果优化前Batches: 2500 SetPass calls: 700 CPU Main Thread: 22ms Render Thread: 15ms优化后Batches: 500 SetPass calls: 160 CPU Main Thread: 13ms Render Thread: 6ms这类提升主要来自材质复用 Static Batching GPU Instancing UI Canvas 拆分13. 实战案例 25000 棵树13.1 错误做法5000 个 GameObject 每个树都 Instantiate 每个树 renderer.material 单独改颜色 每棵树一个材质实例结果Batches 高 内存高 CPU 高 可能还有 GC13.2 改进做法同一个 Tree Mesh 同一个 Tree Material Material 开 Enable GPU Instancing 颜色差异用实例属性 远处树用 LOD 或 Impostor如果树完全不动也可以对一部分使用 Static Batching。但如果树有风动画不要直接把会动的树干/树叶 Static可以考虑GPU Instancing Shader 风动画14. 实战案例 3敌人和战斗假设你后面做单机战斗EnemyPoint 生成 20 个 Enemy Enemy 有 Animator Enemy 会巡逻 Enemy 会攻击 Enemy 有血条不要第一步就追求敌人 GPU Instancing。优先级应该是1. 敌人对象池 2. Animator 参数不要每帧乱 Set 3. AI 逻辑分帧/降频 4. 远距离敌人降低更新频率 5. 血条 UI 只在变化时刷新 6. 同款武器/掉落物/特效再考虑 Instancing为什么因为角色性能常见瓶颈不只在 Draw Call。还可能在Animator SkinnedMeshRenderer 骨骼数量 AI Update 寻路 碰撞检测 UI 血条 特效所以敌人战斗优化不能只盯着合批。15. 常见误区15.1 误区Draw Call 越少越好不绝对。如果你把整个城市合成一个超级大 MeshBatches 降了但可能视锥剔除变差 看不见的东西也被画 内存暴涨 加载变慢正确做法按区域、材质、可见性合理分块15.2 误区勾了 Static 就一定合批不一定。还要看材质 Shader Lightmap Renderer 状态 是否真的 Static15.3 误区物体长一样就能合批不一定。两个物体看起来一样但如果材质不是同一个引用 Shader Keyword 不同 贴图不是同一个 Lightmap 不同 Render Queue 不同都可能不能合。15.4 误区GPU Instancing 能解决所有重复物体不一定。GPU Instancing 喜欢同 Mesh 同 Material 差异用实例数据表达如果每个物体都有完全不同材质、不同 Shader、不同动画它就不适合。15.5 误区Dynamic Batching 一定是优化不一定。Dynamic Batching 会消耗 CPU。如果你的瓶颈本来就在 CPU它可能让情况更糟。16. 项目优化检查清单16.1 场景物体检查固定建筑是否 Static 动态物体是否误 Static 同类物体是否共用材质 贴图是否可以 Atlas 远处物体是否有 LOD 看不见的区域是否能被剔除16.2 材质检查有没有大量 xxx_Instance 材质 有没有代码使用 renderer.material 是否能改为 sharedMaterial 或 MaterialPropertyBlock Shader Keyword 是否过多 透明材质是否太多16.3 UI检查Canvas 是否按变化频率拆分 聊天列表是否对象池 小地图是否每帧无脑刷新 是否每帧 ForceUpdateCanvases 图片是否使用图集 字体材质是否统一 Mask 是否过多16.4 战斗角色检查敌人是否对象池 Animator 参数是否变化时才 Set AI 是否每帧全量计算 血条是否只在血量变化时刷新 远距离敌人是否降频 特效是否对象池16.5 验证每次优化后记录FPS CPU Main Thread Render Thread GPU Time Batches SetPass calls Tris Verts GC Alloc Memory17. 最后总结可以用这张表记住场景优先方案固定建筑、道路、墙Static Batching大量同款树、草、石头GPU InstancingURP 下大量不同物体SRP Batcher很小的低模动态物体Dynamic Batching聊天、背包、HUDCanvas 拆分 图集 对象池玩家、敌人、NPC对象池 LOD 动画/AI 降频一句话合批不是目的稳定帧率才是目的。真正的优化流程应该是发现卡 ↓ Profiler 判断 CPU 还是 GPU ↓ Frame Debugger 看渲染提交 ↓ 只改一个优化点 ↓ 重新测试 ↓ 记录数据这样做你就不会靠感觉优化也不会把项目越改越乱。