Unity Optimization Guide
July 11, 2026 · View on GitHub
How to Read This Guide
This guide targets Unity 2018 through Unity 6 (6000.x), with emphasis on shipping to PC/Steam. Mobile and VR are explicitly out of scope. Every tip that differs meaningfully across engine generations is tagged:
- [Pre-2020] — Unity 2018/2019, Built-in RP era. Much of the workaround folklore on the internet comes from this period. Some of it is now obsolete.
- [2020-2022 LTS] — Unity 2020, 2021, and 2022 LTS. URP and HDRP stable, DOTS Entities 1.0, Addressables as the standard asset management layer, ObjectPool built in.
- [Unity 6+] — Unity 6 (October 2024) and later. GPU Resident Drawer, Render Graph mandatory in URP, STP upscaler, Adaptive Probe Volumes by default, Awaitable mature.
- [All versions] — Applies broadly across all three eras without meaningful change.
Profile first. Every section assumes you have the Unity Profiler open and a concrete marker pointing at the bottleneck. Optimizing without profiling data produces regressions as often as gains. Unity profiling best practices state this plainly: measure before and after every change.
Unity's SRP Batcher is conceptually similar to UE's PSO caching in that both aim to reduce per-draw-call GPU state setup overhead — but they operate differently. The UE guide is your parallel reference for Unreal patterns; this guide mirrors its structure exactly for Unity equivalents.
Glossary
You can find it HERE
Tools
You can find them HERE
Table of Contents
- How to Read This Guide
- Glossary
- Tools
- Guidelines per Specialization
- General Tips
- Code & Mechanics (C# Programmers / Gameplay Engineers)
- Burst, Jobs, and DOTS Programmers
- Level Design / Environment
- Materials / Shader Authors
- Meshes / 3D Art
- Lights & Shadows
- VFX (Particle System and VFX Graph)
- Audio
- Animations
- Physics
- UI (UGUI and UI Toolkit)
- Networking and Multiplayer
- QA / Build / Production
- Tech Art
- Last Resort Methods
- Profiling Workflow Reference
- Optimization Sweep Steps (Pre-Milestone Checklist)
- Top 30 Most Common Mistakes
- Knobs and Settings Cheat Sheet
- Player Settings
- Quality Settings
- URP Asset Key Knobs
- HDRP Frame Settings
- Build Profile Toggles (Unity 2023.1+)
- Quality Scalability Recommendations
- Physics Project Settings Reference
- Audio Project Settings Reference
- Editor Performance Settings Reference (All versions)
- Editor Iteration Performance (All versions)
- Unity ↔ UE Concept Bridge
- Version Migration Notes
- Bibliography and Further Reading
Guidelines per Specialization
General Tips
These apply regardless of discipline. Every specialist should know them.
Common pitfalls:
- [All versions] Caching
Camera.mainis still good practice even post-Unity 2019.4.9, where it was optimized fromFindGameObjectsWithTagto an internal cache. It still crosses the native boundary; cache it inStart(). - [All versions]
GetComponent<T>()inUpdate()is a guaranteed frame-rate destroyer. Every call traverses the component list on that GameObject. Cache inAwake(). - [All versions]
FindObjectOfType<T>(),FindGameObjectsWithTag(), andGameObject.Find()in the update loop are O(n) searches of the entire scene. Never call them per-frame. - [All versions]
Instantiate()andDestroy()in the hot path cause GC pressure, frame spikes, and hitches on lower-end hardware. Everything spawned at runtime must be pooled. - [All versions]
CompareTag("Enemy")overgameObject.tag == "Enemy"— the string property version allocates on every comparison. - [Pre-2020]
new WaitForSeconds(1f)inside a coroutine loop allocates on every iteration. Cache the instance as a static readonly field. - [2020-2022 LTS] Mono backend for shipping builds. Always ship IL2CPP.
Recommended practices:
- Identify CPU vs GPU bound before any optimization. The fix for a CPU bottleneck (reduce Update calls, use Jobs) is completely different from a GPU fix (reduce draw calls, lower shadow distance, drop render scale).
- Use
ProfilerMarkeron every major system boundary so the Hierarchy is meaningful. - Profile on the minimum-spec hardware you ship for, not your dev machine.
- Use
QualitySettings.SetQualityLevel()to expose Low/Medium/High/Ultra tiers to players. Ship with at least three tiers. - Use
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]for global service initialization that must survive scene transitions. - Prefer structs over classes for frequently instantiated small objects (Vector3, Matrix4x4, custom data containers) to reduce GC pressure.
- Use
sqrMagnitudefor distance comparisons —(a - b).sqrMagnitude < range * rangeavoids asqrtcall. - Enable Enter Play Mode Settings (Disable Domain Reload + Disable Scene Reload) immediately on any project with more than a handful of scripts — the daily iteration time saved is enormous. [Unity 2019.3+]
- Ship with
QualitySettings.vSyncCount = 1as the default, not 0. Uncapped frame rate on a PC with a fast GPU can drive temperatures up and cause thermal throttling on the minimum-spec machine during profiling, masking real numbers. - Use
Application.runInBackground = falsein shipped builds unless your game needs background execution. Every frame saved when the window is unfocused is CPU you get back. - The Unity Profiler's
Othersbar in Play mode represents EditorLoop work — largeOthersin editor profiling is not game overhead and does not appear in builds. Do not optimize against it. - For any object that needs to be found by other systems, register it with a central service locator or event system at
Awake(). Never search for it withFindObjectOfType<T>()later. Time.timeScale = 0pauses the game butFixedUpdatestill runs at zero-time steps andUpdatestill ticks. If your game has a pause menu with expensive scripts, disable irrelevant MonoBehaviours explicitly on pause.- Profile the first frame separately from steady-state. First-frame costs (shader compilation, texture upload, audio loading) can spike to 200 ms or more and are invisible in a steady-state capture.
Cheat sheet — cross-discipline quick wins:
| Action | Time cost | Discipline |
|---|---|---|
| Disable Domain Reload | 5–30 s per play press | All |
| Add asmdef files | 60–80% compile time reduction | Programmers |
| IL2CPP "Faster runtime" | Ship faster startup | QA/Build |
CullCompletely on off-screen Animators | Saves per-NPC animation eval | Animation |
| Disable Raycast Target on decorative UI | Input iteration cost | UI |
| Set VFX Graph bounds to Manual | Enables off-screen culling | VFX |
| Force to Mono on all 3D audio | Halves audio memory | Audio |
shader_feature_local over multi_compile | Strips unused variants | Materials |
| LOD Bias 0.7 instead of 1.0 | Earlier LOD transitions, lower triangle count | Level Design |
| BC5 for normal maps | Better quality at same size | Art/Tech Art |
Code & Mechanics (C# Programmers / Gameplay Engineers)
Gameplay code is the most common source of CPU-bound frame-rate problems in indie projects. The following covers the patterns that matter most at production scale.
Common pitfalls:
- [All versions]
Camera.maininUpdate()— still a native boundary crossing. Cache inStart(). JetBrains Rider Camera.main docs - [All versions]
GetComponent<T>()inUpdate()without caching. Scene-traversal operation on every call. - [All versions]
Instantiatewithout pooling — causes GC spikes and frame hitches on spawn. - [All versions] String concatenation in
Update()— immutable strings, every concatenation allocates. - [All versions] LINQ in hot paths — every expression allocates at minimum one enumerator object.
- [All versions]
foreachover anIEnumerable<T>interface variable boxes the struct enumerator. Use concreteList<T>. - [Pre-2020]
new WaitForSeconds()inside a coroutine loop — allocates every iteration. Cache asprivate static readonly. - [2020-2022 LTS] Lambda closures capturing local variables in hot paths. Compiled to heap-allocated closure objects.
- [All versions]
Update()on 10,000+ MonoBehaviours — native-to-managed interop overhead at scale. Custom tick manager 4–11× faster.
Recommended practices:
- Use
UnityEngine.Pool.ObjectPool<T>(Unity 2021+) for all runtime-spawned objects:
using UnityEngine.Pool;
private IObjectPool<Bullet> _pool;
void Awake()
{
_pool = new ObjectPool<Bullet>(
createFunc: () => Instantiate(bulletPrefab),
actionOnGet: b => b.gameObject.SetActive(true),
actionOnRelease: b => b.gameObject.SetActive(false),
actionOnDestroy: b => Destroy(b.gameObject),
defaultCapacity: 20,
maxSize: 100
);
}
- Implement a tick manager pattern for scenes with hundreds of MonoBehaviours with
Update(): one singleton with aList<IUpdatable>, one native-to-managed crossing per frame regardless of subscriber count. Unity 6 manual example - Cache
LayerMask.GetMask()results asprivate static readonly int— string hashing per call otherwise. - Use
transform.GetPositionAndRotation(out pos, out rot)[Unity 2021.3+] to halve native crossings when reading both values. - Cache
Animator.StringToHash("ParameterName")as static readonly ints, pass int overloads ofSetFloat,SetBool. - Reserve
FixedUpdatestrictly for physics operations (Rigidbody,AddForce). Non-physics logic inFixedUpdateadds unnecessary simulation steps. - Use
Destroy(go, delay)instead of a delay coroutine — built-in, no coroutine, no allocation. - Use
Span<T>andstackallocfor small temporary buffers [Unity 2021+] to avoid heap allocation entirely:
Span<int> ids = stackalloc int[64];
FillIds(ids);
ProcessIds(ids);
- Enable Incremental GC in Player Settings (Unity 2019.1+) as a safety net, but do not treat it as an excuse for allocation-heavy code.
- Use
Physics.autoSyncTransforms = falsein physics-heavy scenes and sync manually before queries:
Physics.autoSyncTransforms = false;
// ... move objects ...
Physics.SyncTransforms();
var hit = Physics.Raycast(origin, dir, out RaycastHit info);
TheGameDev.Guru physics autoSyncTransforms
Lesser-known tricks:
Application.targetFrameRateis ignored when VSync is enabled. Disable VSync first if you want to cap differently.Time.captureFramerate = 60forcesTime.deltaTimeto always return1f / 60, enabling screenshot-based cinematics at consistent timings regardless of real elapsed time.HideFlags.HideInHierarchy | HideFlags.DontSavehides manager GameObjects from designers and prevents them being included in scene serialization.[ExecuteAlways]runs in both editor and built players;[ExecuteInEditMode]is legacy editor-only. PreferOnValidatefor inspector-change responses —[ExecuteAlways]runningUpdate()continuously in edit mode wastes editor performance.List<T>.ForEachwith a capturing lambda allocates per call. A plainforloop with index is always allocation-free.StopCoroutineshould target a storedCoroutinereference, notStopAllCoroutines()— the latter stops coroutines started by other systems on the same object.- Using
#if DEVELOPMENT_BUILDto strip debug logging and profiling overhead adds zero cost to release builds. UseScripting Define Symbolsfor project-specific compile-time gates.
Tools and profiling for this role:
- CPU Profiler Hierarchy → sort by GC Alloc to find allocators. Magenta bars in Timeline = GC.Alloc.
- Enable Allocation Call Stacks in the Profiler toolbar to trace each GC.Alloc to its source line.
- Deep Profile a 2–5 second capture targeting the suspect system — not the whole session.
ProfilerMarkeron every major system boundary: AI, Pathfinding, Combat, Spawning.- Memory Profiler package → Objects and Allocations → filter to "Managed" for heap analysis.
Cheat sheet:
| Pattern | Allocation? | Fix |
|---|---|---|
Camera.main in Update | Native crossing | Cache in Start() |
GetComponent<T>() in Update | Scene traversal | Cache in Awake() |
new WaitForSeconds(t) in coroutine loop | Yes | private static readonly WaitForSeconds |
gameObject.tag == "x" | String allocation | CompareTag("x") |
foreach (var x in iEnumerable) | Boxing | foreach over concrete List<T> |
| LINQ in Update | Enumerator + collections | Manual for loop |
| Lambda capturing locals in hot path | Closure object | Static method + explicit params |
Instantiate/Destroy in tick | GC + CPU spike | ObjectPool<T> |
Burst, Jobs, and DOTS Programmers
This section is for developers choosing to adopt Unity's data-oriented stack. ECS is not required for most indie games — it earns its complexity at thousands of entities sharing similar archetypes.
Common pitfalls:
- [2020-2022 LTS] Applying ECS to small entity counts where cache benefits don't materialize. The archetype/chunk overhead breaks even at roughly 500–1000 entities of the same type.
- [2020-2022 LTS] Using
SystemBaseinstead ofISystemfor performance-critical systems —SystemBaseis a managed class, not Burst-compilable on system methods. - [2020-2022 LTS]
Entities.ForEach— deprecated in Entities 1.0. Migrate toSystemAPI.Query<T>()(main thread) orIJobEntity(parallel). Entities 1.0 SystemAPI.Query - [2020-2022 LTS] Calling
Complete()on aJobHandleimmediately after scheduling — defeats the purpose of the Job System. Schedule early, complete as late as possible each frame. - [2020-2022 LTS]
NativeListgrowth during parallel execution. Pre-size with an upper bound; useAsParallelWriter()pattern. - [2020-2022 LTS] Forgetting to
Dispose()NativeArray<T>and other NativeContainers. In Editor, Unity throws exceptions. In release builds, leaks are silent. - [2020-2022 LTS] Using
UnityEnginemath types (Vector3,Quaternion) in Burst jobs instead ofUnity.Mathematicsequivalents — may miss SIMD optimization opportunities.
Recommended practices:
- Mark all job structs
[BurstCompile]. For maximum SIMD utilization, addFloatMode.Fastwhere determinism is not required:
[BurstCompile(FloatPrecision.Standard, FloatMode.Fast)]
public struct VelocityJob : IJobParallelFor
{
public NativeArray<float3> Positions;
[ReadOnly] public NativeArray<float3> Velocities;
public float DeltaTime;
public void Execute(int i)
{
Positions[i] += Velocities[i] * DeltaTime;
}
}
- Choose allocator correctly:
Allocator.Temp(fastest, single frame, cannot pass to job fields),Allocator.TempJob(fast, ≤4 frames, must Dispose),Allocator.Persistent(unlimited, must Dispose manually — leaks are silent). NativeContainer allocators guide - Chain
JobHandledependencies explicitly.JobHandle.CombineDependenciesmerges multiple handles. The Dependency property onISystem(state.Dependency) automatically chains system dependencies in ECS. - Prefer
ISystemoverSystemBasefor all performance-critical ECS systems:
[BurstCompile]
public partial struct MovementSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float dt = SystemAPI.Time.DeltaTime;
foreach (var (transform, velocity) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<Velocity>>())
{
transform.ValueRW.Position += velocity.ValueRO.Value * dt;
}
}
}
Chunk iteration in Entities 1.0
- Prefer
IJobEntityoverIJobChunkfor per-entity work — it is higher-level and generates the same Burst-compiled code. Unity Job System and Burst guide - Mark input data
[ReadOnly]in jobs — enables parallel reads without race condition checks and gives Burst additional optimization opportunities. - Use
Unity.Mathematicstypes (float3,float4,quaternion) for full SIMD vectorization.UnityEngine.Vector3can be used in Burst jobs but may miss opportunities. Burst docs - Wrap any debug logging needed in Burst jobs with
[BurstDiscard]:
[BurstDiscard]
static void DebugLog(string msg) => Debug.Log(msg);
- For Subscene baking, ensure bakers are stateless — never cache values on the baker instance; incremental baking calls bakers on re-run. Entities 1.0 baker overview
- Use Entities Graphics (
com.unity.entities.graphics) for rendering ECS entities via BRG. Requires URP or HDRP with Forward+ path. Entities Graphics docs
Lesser-known tricks:
- Burst Inspector (
Jobs → Burst Inspector) shows generated assembly for any compiled job. Look for SIMD register usage (ymm,xmmon x86) to confirm auto-vectorization. Absence of SIMD indicates a struct alignment or managed-type dependency is blocking it. - Burst 1.6+ supports
[BurstCompile]on static methods for direct calls without job overhead — useful for math utility functions called from managed code. NativeArray.Dispose(jobHandle)ties disposal to a JobHandle completing — safer than separate Dispose calls. Prevents "collection still in use" bugs.[NativeDisableParallelForRestriction]overrides safety checks for intentionally distinct-index parallel writes. Incorrect use = silent data races in release builds.- DOTS Physics and Havok Physics share the same data protocol — you can switch between them without rewriting content. ECS Physics — Havok vs Unity Physics
- The Job System's
innerloopBatchCountparameter inIJobParallelFor.Schedule(count, batchCount, deps)controls granularity. Values of 32–128 are typical; too small increases scheduling overhead, too large reduces CPU core utilization.
Tools and profiling for this role:
- CPU Profiler Timeline → Worker threads — are all cores utilized during job execution? Idle workers indicate scheduling bottlenecks.
- Burst Inspector for SIMD verification.
- DOTS Entities Hierarchy window (Window → Entities → Hierarchy) for entity count and archetype inspection.
- Profiler → Physics module for Unity Physics cost.
Cheat sheet:
| Allocator | Speed | Max lifetime |
|---|---|---|
Temp | Fastest | 1 frame; cannot pass to job fields |
TempJob | Fast | ≤4 frames; must Dispose() |
Persistent | Slow | Unlimited; leaks silently in release |
Level Design / Environment
Level designers directly control the largest contributors to both CPU and GPU frame time through scene structure and asset density.
Common pitfalls:
- [All versions] Occlusion Culling baked on open terrain or large open-world scenes — bake time is prohibitive, data is large, and the open-sky visibility defeats the purpose. Only effective in closed environments.
- [All versions] Missing or improperly configured LOD Groups on hero assets — a character visible at 500 m still rendering at LOD0 polygon count.
- [All versions] Camera far plane set too large for the scene type — a corridor game with a far plane of 1000 m wastes GPU on frustum culling and shadow calculations for geometry the player will never see.
- [2020-2022 LTS] Manual Light Probe Group placement leaving large unlit gaps — dynamic objects flicker between probes or go dark.
- [Unity 6+] Not enabling GPU Resident Drawer on scenes with hundreds of repeated mesh-material pairs.
- [All versions] ProBuilder meshes used directly in production without conversion to static mesh assets — no automatic LOD, poor occlusion culling cell generation on large flat faces.
- [All versions] Terrain with more than 4 texture splat layers per visible tile — each group of 4 layers adds a draw pass.
Recommended practices:
- Configure LOD Groups with screen-space percentage thresholds calibrated for your typical play distance. Recommended starting values:
- LOD0: ≥ ~30% screen height (close range)
- LOD1: 50% polycount reduction
- LOD2: 80% polycount reduction
- Culled: < ~1% screen height
QualitySettings.lodBiasat 0.7–0.85 often hits the best quality-performance balance on PC. Unity LOD Group docs
- Enable
Fade Modeon LOD Groups for visual cross-fading — hides LOD pop-in without performance cost. - Bake Occlusion Culling only for enclosed environments. Use Occlusion Areas to define cells and Occlusion Portals for openings (doors, archways). Unity Occlusion Culling docs
- [Unity 6+] Use GPU-based occlusion culling (URP/HDRP, depth-buffer reprojection, no baking) for scenes using GPU Resident Drawer. Unity Manual – GPU Occlusion Culling
- Use Addressables-based additive scene loading for world streaming:
Addressables.LoadSceneAsync/Addressables.UnloadSceneAsync. AvoidResources.Load— synchronous and not memory-managed. - Enable Draw Instanced on Terrain components (
terrain.drawInstanced = true) [Unity 2021+] — batches terrain chunks into fewer draw calls. - Keep Terrain splat layers ≤ 4 per visible tile. Each additional group of 4 layers adds a draw pass. For complex biomes, consider separate Terrain tiles rather than one tile with 8 layers.
- Set Camera near/far planes to the minimum required for your scene type. Shadow Distance (Quality Settings) is separate from the camera far plane and is usually the larger contributor to GPU shadow cost.
- [Unity 6+] Prefer Adaptive Probe Volumes (APV) over manual Light Probe Groups for complex scenes. APV placement is automatic and sampling is per-pixel rather than per-object. Enable: Quality Settings → Render Pipeline Asset → Light Probe System → Adaptive Probe Volumes. Unity APV usage guide
- [All versions] Place Reflection Probes at eye level in distinct reflective zones. Use cubemap resolution 128–256 for most probes; reserve 512 for hero materials. Enable Box Projection for interior environments to avoid "infinitely far away" reflections.
Lesser-known tricks:
- APV light leakage through thin walls is common. Mitigate with Adjustment Volumes (separate indoor/outdoor zones), thicker geometry in problem areas, or manual probe override volumes. APV vs Light Probes — Reddit
StaticBatchingUtility.Combinecan be called at runtime on non-Static objects to force a combined mesh batch — useful for dynamically instantiated sets of props that won't move.- Billboard trees have
Billboard Startdistance that controls when 3D trees switch to impostors. Push this distance as close to the camera as visually acceptable — 3D trees at 200 m contribute significant triangle and shadow cost. - Occlusion Portal components model door openings dynamically. A closed door toggles occlusion on/off — useful for dungeon games with many connecting corridors.
- [Unity 6+] APV supports Light Blending Scenarios — bake multiple lighting states (day, night, indoor/outdoor) and blend between them at runtime without re-baking.
- ProBuilder meshes for whitebox: subdivide large flat faces before baking Occlusion Culling. Very large single faces prevent proper PVS cell generation. Occlusion Culling with ProBuilder — Reddit
Tools and profiling for this role:
- Rendering Profiler module → visible renderers, shadow casters, triangles per frame.
- GPU Profiler → Shadows (excessive shadow distance), Geometry (missing LODs).
- Frame Debugger → inspect batching and LOD transitions.
- Rendering Debugger (URP/HDRP: Window → Analysis → Rendering Debugger) → Lighting → Shadow Cascades view for cascade boundary visualization.
Cheat sheet:
| Setting | Default | Recommended for PC |
|---|---|---|
QualitySettings.lodBias | 1.0 | 0.7–0.85 for performance |
QualitySettings.shadowDistance | 150 | 80–120 for most PC games |
Terrain drawInstanced | false | Enable (Unity 2021+) |
| Reflection Probe resolution | 128 | 128–256 standard, 512 hero |
| Cascade count (URP) | 4 | 2–3 for mid-range targets |
Materials / Shader Authors
Shader authors have disproportionate control over GPU performance and the SRP Batcher. The decisions made here affect every frame.
Common pitfalls:
- [2020-2022 LTS]
MaterialPropertyBlockbreaking SRP Batcher compatibility — any renderer using MPB falls back to GPU Instancing (if shader supports it) or unoptimized draw calls. SRP Batcher deep dive — fazz.dev - [All versions] Shader keyword variant explosion. 10 feature flags = 1,024 variants.
#pragma multi_compilenever strips unused variants automatically. - [Pre-2020] Using
multi_compilewhereshader_featurewould suffice —shader_featurestrips variants not used by any material in the build. - [All versions] Global keywords instead of local keywords — global keyword space is limited (~196 available after Unity's own usage in 2020+).
- [All versions] Sampler limit: max 16 samplers per HLSL shader stage. Hitting this limit causes silent fallback behavior.
- [Pre-2020] Surface Shaders — not supported in URP or HDRP at all. Legacy Built-in RP only.
- [2020-2022 LTS] Custom Function Node in Shader Graph with non-deterministic output breaking SRP Batcher.
Recommended practices:
- For SRP Batcher compatibility, all per-material properties must be declared in a single
UnityPerMaterialCBUFFER:
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
float _Smoothness;
float _Metallic;
CBUFFER_END
SRP Batcher shader requirements
- Use
shader_feature_local(notmulti_compile) for features toggled via materials at bake time — Unity strips unused combinations:
#pragma shader_feature_local _EMISSION
#pragma shader_feature_local _NORMALMAP
- Use
multi_compile_localonly for keywords enabled at runtime viaShader.EnableKeyword. Always local, never global. - Restrict keyword declarations to specific shader stages to reduce variant count:
#pragma shader_feature_fragment_local FEATURE_A FEATURE_B
- Implement
IPreprocessShaders.OnProcessShaderin an Editor script for surgical variant stripping at build time. Use whitelist/blacklist workflow. TheGameDev.Guru variant stripping - Use Shader Variant Collection +
ShaderVariantCollection.WarmUp()to pre-compile critical variants before gameplay begins. Unity 6 adds a Shader Pipeline Library (analogous to Vulkan pipeline cache) to persist compiled shaders across sessions. - For per-instance variation without breaking SRP Batcher: create a unique material per variant (cheap with SRP Batcher since same-shader materials batch together) OR encode variation as a per-instance buffer via BRG in Unity 6+. Reserve
MaterialPropertyBlockfor specific cases where GPU Instancing is the intended path. - Handle the 16-sampler limit with shared sampler states:
TEXTURE2D(_AlbedoTex);
TEXTURE2D(_NormalTex);
SAMPLER(sampler_linear_repeat); // shared — counts as one sampler
- Use Texture2DArray to pack N textures of the same dimensions/format into one GPU resource and one sampler binding. Access via
float3(uv.x, uv.y, arrayIndex). Unity BCn texture compression - For GPU Instancing pragmas when using
DrawMesh*APIs, add to the shader:#pragma instancing_options assumeuniformscaling nolightmap nolodfade nolightprobe. - In Shader Graph, wrap Custom Function Nodes in Sub Graphs for reuse — Custom Function Nodes themselves cannot be reused directly. Custom Function Node docs
Lesser-known tricks:
- Right-click any Shader Graph asset → See Generated Code to inspect the HLSL output and count variant macros.
ShaderLabvs Shader Graph choice: Shader Graph auto-generates SRP Batcher–compatible CBUFFERs and handles render pass tags — it is safer for material artists. Hand-written HLSL is still needed for custom render passes, fullscreen effects, and procedural geometry.- Verify SRP Batcher batching in Frame Debugger: expand the SRP Batcher node. "Batch reason: different material" means materials differ; "different shader" means base shaders differ — the latter cannot batch even with the same material. SRP Batcher batches objects sharing the same shader.
- Normal maps should always use BC5 format on desktop targets — it independently compresses X and Y channels, superior quality vs BC1/DXT1, Z reconstructed in shader as
sqrt(1 - x² - y²). BCn analysis — Nathan Reed
Tools and profiling for this role:
- Frame Debugger → SRP Batcher section: batch sizes and break reasons.
log shader compilationin Player Settings to count variants at build time.- Burst Inspector does not apply here, but NVIDIA Nsight Graphics for shader occupancy and warp utilization.
- Rendering Debugger → Material Validation for overdraw and SRP Batcher compatibility.
Cheat sheet:
| Feature | Use shader_feature | Use multi_compile |
|---|---|---|
| Toggled per-material at bake time | Yes | No |
| Enabled at runtime via script | No | Yes |
| Auto-stripped if no material uses it | Yes | No |
| Keyword scope | Max count | Default |
|---|---|---|
| Global (avoid) | ~196 available | Was default Pre-2021 |
Local (_local) | 64 per shader | Use always |
Meshes / 3D Art
Mesh import decisions have permanent memory and rendering consequences. Most are set once and forgotten — which is how mistakes persist for years.
Common pitfalls:
- [All versions] Read/Write Enabled left on imported meshes — keeps a CPU-side copy in system RAM, doubling mesh memory. Only needed when accessing
Mesh.verticesor performing runtime mesh modification from C#. - [All versions] Read/Write Enabled on textures — same issue: doubles VRAM+RAM. Unity texture import docs
- [All versions] 32-bit index buffers on meshes under 65,535 vertices — wastes memory and bandwidth. Default to 16-bit unless the mesh genuinely needs it. Mesh.indexFormat docs
- [All versions] Missing Compress Mesh — uncompressed vertex data (position, normals, UVs) wastes memory. Minor precision loss is rarely visible.
- [All versions] No LOD Group on hero assets — a distant character at LOD0 polygon count is a consistent GPU cost.
- [2020-2022 LTS] Static Batching enabled alongside GPU Resident Drawer — they conflict; disable Static Batching when using GRD.
Recommended practices:
- Disable Read/Write Enabled on all mesh import settings by default. Enable only for meshes accessed via C# at runtime.
- Enable Compress Mesh in import settings. Quantizes vertex positions and normals to 16-bit. Rarely visible in game.
- Set Index Buffer Format to 16-bit for meshes under 65K vertices. Use 32-bit only when genuinely needed.
- Configure LOD Groups with LOD0 (full detail), LOD1 (50% reduction), LOD2 (80% reduction), Culled (< 1% screen height). Enable Fade Mode for smooth transitions.
- [Unity 6.2+] Enable Mesh LOD in import settings for automatic LOD generation. Unity stores all LOD levels in the original mesh index buffer sharing the same vertex buffer, significantly reducing memory overhead vs separate LOD meshes. Unity 6 Mesh LOD
- Use
Mesh.MarkDynamic()for procedural/runtime-updated meshes — hints the GPU driver to use faster buffer update paths:
mesh.MarkDynamic(); // call once after creation, before per-frame vertex writes
- Use
StaticBatchingUtility.Combineto manually force-combine a set of runtime-instantiated static props that won't move. - For GPU Skinning: enable in Player Settings → Other Settings → GPU Skinning. Offloads skinned mesh deformation to a Compute Shader pass. Unity 6 introduced Batched GPU Skinning — reduces the number of separate Compute dispatches for groups of identically-configured skinned meshes. DOUBLE Unity Animation Performance
- Reduce blend weights in
Quality.blendWeights(1, 2, or 4 bones per vertex). Four bones is the humanoid default; two is usually indistinguishable at LOD1 and beyond. - Strip scale curves in animation clips at import if the character never scales — scale curves are more expensive than translation and rotation curves.
Lesser-known tricks:
CombineMeshesin C# does not interact with the Unity batching system — you manage the combined mesh as a runtime asset. Best for static environmental detail meshes that will never move and share one material.- Meshes generated with vertex colors for GPU Instancing per-instance tinting avoid creating separate materials per color variant, keeping SRP Batcher batches intact — but only if the shader uses the vertex color as the instanced parameter.
- Optimize Game Objects in rig import settings collapses the bone hierarchy, removing individual GameObjects per bone. Saves transform update overhead in crowds. Disable if scripts need direct access to bone transforms.
- Compute shaders always load indices as 32 bits — if using 16-bit indices in a compute buffer, unpack accordingly. This is a non-obvious mismatch that can corrupt compute shader output.
Tools and profiling for this role:
- Rendering Profiler module → triangles/vertices per frame.
- Memory Profiler → Objects by type → Mesh category.
- Frame Debugger → identify which meshes are generating unexpected draw calls.
- GPU module → Geometry section for vertex processing cost.
Cheat sheet:
| Import Setting | Default | Recommended |
|---|---|---|
| Read/Write Enabled | Off | Keep off; enable only when needed |
| Compress Mesh | On | On |
| Index Format | 16-bit | 16-bit; 32-bit only for >65K verts |
| GPU Skinning | On | On |
| Optimize Game Objects | Off | On for crowds |
Lights & Shadows
Lighting is consistently the second-largest GPU cost in Unity projects after geometry, and shadow map rendering is frequently the largest single render pass.
Common pitfalls:
- [All versions] Shadow distance set too wide —
QualitySettings.shadowDistanceof 150 m in a corridor game casts shadows for geometry the player will never see. - [All versions] Too many shadow-casting real-time lights — each additional shadow-casting light adds a full shadow map render pass.
- [All versions] Realtime Reflection Probes updating every frame — use
OnDemandrefresh unless reflections are dynamic. - [Pre-2020] Enlighten Realtime GI still enabled — deprecated in 2019, removed from HDRP in 2020.1. Enlighten deprecation docs
- [2020-2022 LTS] Manual Light Probe Groups leaving dark seams on dynamic objects transitioning between probe groups.
- [Unity 6+] APV light leakage through thin walls — mitigation required for interiors.
Recommended practices:
- Use Shadowmask Mixed Lighting mode for PC desktop games — the best balance between quality and runtime performance. Near-camera: real-time shadows. Far: baked shadowmask texture. Up to 4 lights share a shadowmask (one per RGBA channel). Unity Mixed lighting docs
| Mixed Light Mode | Shadows | Cost | Best for |
|---|---|---|---|
| Baked Indirect | Real-time shadow map | Medium | High quality, stable lighting |
| Shadowmask | Near real-time, far baked | Medium-high | Best quality/perf balance |
| Subtractive | Fully baked, 1 directional | Low | Stylised games, lowest cost |
- Configure shadow cascades in the URP Asset → Shadows section. 2–3 cascades for mid-range PC targets; 4 for high-end. Visualize boundaries with Rendering Debugger → Lighting → Shadow Cascades. Higher cascade count improves quality at cost of additional render passes. Shadow cascades Unity 6
- Use PCF (Percentage Closer Filtering) for soft shadows in URP. The URP Screen Space Shadows Renderer Feature moves shadow resolve to screen space before the opaque pass — reduces aliasing at lower cascade counts.
- [Unity 6+] Enable Adaptive Probe Volumes (APV) instead of manual Light Probe Groups:
- Probe placement: automatic (geometry density)
- Sampling: per-pixel (eliminates seams at object boundaries)
- Memory streaming: yes (open worlds)
- Enable: Quality Settings → Render Pipeline Asset → Light Probe System → APV APV concept docs
- [All versions] Place Reflection Probes at eye level in distinct reflective zones. 128–256 resolution for most; 512 for hero materials. Enable Box Projection for interiors.
- Use Rendering Layers (URP 2021+, HDRP 2019.3+) to restrict which lights affect which renderers — reduces per-object lighting cost in scenes with many lights:
// Assign layers in URP Asset, per-light and per-renderer Rendering Layer Mask
renderer.renderingLayerMask = LayerMask.GetMask("CharacterLayer");
- For baking: Unity Progressive GPU Lightmapper for fast iteration; Bakery (paid) for higher quality, faster bake times, and IES light support. Community consensus in 2025: Bakery still produces better results and bakes significantly faster. Is Bakery still better in Unity 6.2?
Lesser-known tricks:
- SSAO in URP runs as a Renderer Feature before opaque objects are shaded. Disabling it for Low quality tier is a high-leverage GPU save.
- Disable
#pragma enable_d3d11_debug_symbolsin production builds — even as a comment it can affect shader compilation in some configurations. - HDRP Volumetric Fog cost scales with froxel grid resolution.
Depth Extentreduces frustum slice count;Fog Control Mode → Balanceadjusts quality/perf on a 0–1 slider;Directional Lights Onlyhalves cost for most scenes. HDRP Fog docs - APV supports Light Blending Scenarios — pre-bake multiple lighting states (day/night, indoor/outdoor) and blend between them at runtime without re-baking.
- In [Pre-2020] projects, Enlighten Realtime GI was the only dynamic indirect solution. For Unity 6+, APV is the answer for baked scenarios; HDRP Screen Space GI covers fully dynamic.
Tools and profiling for this role:
- GPU Profiler → Shadows, Lighting sections.
- Rendering Debugger → Shadow Cascades view, Light Overlays.
- Frame Debugger → shadow map render passes (how many passes for how many shadow-casting lights?).
- RenderDoc for shadow atlas inspection.
Cheat sheet:
| Setting | Performance impact | Recommendation |
|---|---|---|
| Shadow Distance | High | 80–120 m for most PC games |
| Cascade Count | Medium | 2–3 for mid-range, 4 for high-end |
| Reflection Probe Update | High (Realtime) | Use Baked or OnDemand |
| SSAO sample count | Medium | Reduce for Low quality tier |
| Mixed Lighting Mode | Medium | Shadowmask for PC |
VFX (Particle System and VFX Graph)
Visual effects are one of the most asymmetric performance areas in Unity: they can look identical at 1% of the GPU cost, or destroy frame rate with the wrong configuration.
The two systems:
| Feature | Particle System (Shuriken) | VFX Graph |
|---|---|---|
| Execution | CPU | GPU (Compute Shader) |
| Platform requirement | All | Compute Shader support required |
| Particle ceiling | Thousands | Millions |
| Physics integration | Full Unity physics | Depth-buffer + custom |
| Render pipeline | Built-in, URP, HDRP | URP, HDRP (no Built-in) |
| Recommended for | Small-scale, physics-heavy | Large-scale, GPU-rich targets |
Unity Manual – Choosing your particle system solution
Common pitfalls:
- [All versions] VFX Graph bounds set to Automatic — forces
Culling Flag: Always recompute bounds and simulate, disabling culling entirely. Unity Manual – Visual Effect Bounds - [All versions] Particle System Collision module enabled — world collision requires physics queries every frame per particle. By far the most expensive Particle System module.
- [All versions] Sub-emitters chained 3+ deep on dense burst systems — each sub-emitter is a separate CPU-evaluated system.
- [All versions] Light module enabled — each particle that spawns a light adds a real-time light to the scene. Limit to ≤ 4 simultaneous.
- [All versions] Trail module on dense systems — trail geometry rebuilds every frame; keep trail counts below a few hundred per system.
- [All versions] Large VFX Graph with
Automaticbounds on many simultaneous instances — each instance simulates even when completely off-screen.
Recommended practices:
- Set VFX Graph bounds to Manual for final builds:
Initialize Context → AABox attribute
- Width: {max effect width}
- Height: {max effect height}
- Depth: {max effect depth}
Use Recorded bounds during development (VFX Control panel → Bounds Recording → Apply Bounds), then convert to Manual. Use Bounds Padding on the Initialize Context to add per-axis slack for output-stage scaling (stretched quads extend beyond simulation bounds).
- Prefer Plane-mode collision in Particle System over World collision. For visual-only effects, switch to VFX Graph depth-buffer collision.
- Cap the Particle System Light module to a Max Light Count in the module settings — prevents spikes during dense burst moments.
- Use VFX Graph's C# Event API and Property Bindings for game logic integration at near-zero allocation cost:
var vfxComp = GetComponent<VisualEffect>();
vfxComp.SetFloat("SpeedScale", currentSpeed);
// Event with attribute payload
var attr = vfxComp.CreateVFXEventAttribute();
attr.SetVector3("HitNormal", hit.normal);
vfxComp.SendEvent("OnHit", attr);
- For Particle System light probe sampling per-particle: use Blend Probes mode in the Renderer module and ensure Light Probe Group density is adequate in the VFX area. Light probes sample at system origin by default, not per-particle.
- Output Mesh (VFX Graph): use when particle overdraw is the bottleneck — mesh output provides real depth, reducing fill-rate at the cost of vertex count scaling linearly with particle count. Profile fill-rate savings vs vertex cost.
- Output Quad default: add
Soft Particleblend to reduce hard depth-intersection edges without extra geometry.
Lesser-known tricks:
- VFX Graph has a
Capacitysetting that sets the maximum particle count. Setting this too high wastes GPU memory even if particles never reach that count. Keep it the minimum required for the effect. - VFX Graph
Culling Flagscontrol the simulation mode:Recompute bounds and simulate(always),Only simulate when visible,Stop simulating when invisible. Set to the latter for ambient effects that should not tick off-screen. - Particle System's Prewarm option simulates the system forward in time on enable — avoids the "cold start" visual artifact but costs one expensive update frame at initialization. For pooled systems,
Stop()and re-Play()without prewarm on reuse. - VFX Graph GPU events (from Update Context to Spawn Context) can trigger child particle spawns entirely on the GPU, eliminating CPU round-trips for complex multi-stage effects.
Tools and profiling for this role:
- Particle Profiler section in the CPU Profiler module.
- Profiler → GPU module → Geometry section for VFX Graph vertex cost.
- Frame Debugger for overdraw analysis on transparent particle quads.
- VFX Control panel in the Editor for bounds recording and particle count visualization.
Cheat sheet:
| Module / Setting | Cost | Recommendation |
|---|---|---|
| Collision (World) | Very High | Avoid; use Plane mode or VFX Graph depth collision |
| Sub-emitters (3+ deep) | High | Profile; flatten chains |
| Light module | High | Cap Max Light Count |
| Trails | Medium | Cap count; strip width at distance |
| VFX Bounds: Automatic | Disables culling | Use Manual for final builds |
| VFX Capacity | GPU memory | Set to minimum required |
Audio
Audio is rarely the primary frame-rate bottleneck but frequently contributes to memory bloat, unexpected CPU spikes, and subtle frame-time inconsistencies.
Common pitfalls:
- [All versions] Long music files set to Decompress on Load — decompresses the entire file to PCM on load, consuming massive RAM for no runtime benefit. Music must use Streaming.
- [All versions] Stereo clips on 3D spatialised sources — stereo information is discarded by spatialisation anyway. Enable Force to Mono on all 3D audio sources.
- [All versions] Voice limit not configured — Unity's default is 32 concurrent voices. Voices beyond the limit are stolen by priority. Voices playing silently still count against the limit.
- [All versions] AudioSource components not disabled when sources are beyond audible range — silent voices still consume voice budget.
- [All versions] Heavy DSP effects (convolution reverb, FFT-based effects) on many Mixer groups — DSP CPU cost scales with active effect count.
Recommended practices:
- Apply the correct load type per clip category: Unity Manual – AudioClip
| Load Type | Memory | CPU cost | Best for |
|---|---|---|---|
| Decompress on Load | High (raw PCM) | Minimal | Short one-shots, UI sounds |
| Compressed in Memory | Low | ~0.5% per Vorbis voice | Medium-length SFX, foley |
| Streaming | Minimal (~200 KB) | Separate thread | Music, long ambient loops |
- Apply correct compression format per clip:
| Format | Compression | CPU cost | Best for |
|---|---|---|---|
| PCM | 1:1 (uncompressed) | Negligible | Shortest effects, UI |
| ADPCM | ~3.5× | Very low | High-frequency noisy sounds (footsteps, impacts) |
| Vorbis/MP3 | ~5–50× | Medium | Music, dialogue, longer SFX |
Game Developer – Unity Audio Import Optimisation
- Enable Force to Mono aggressively on all 3D audio sources. Re-check normalization after applying — stereo→mono conversion can reduce perceived loudness.
- Reduce sample rate from 44.1 kHz to 22.05 kHz for most SFX — halves raw data size with negligible audible impact.
- Set
AudioSource.priorityexplicitly: 0 = highest, 256 = lowest. Dialogue: 0–64. Ambient loops: 200+. - Disable
AudioSourcecomponents when beyond audible range — do not rely on volume falloff alone. - Use
AudioMixerfor group routing, DSP effects, and snapshot transitions. Monitor DSP CPU cost in the Audio Profiler module. Side-chain compression (duck music under dialogue):Sendeffect on source group +Receive + Duck Volumeon destination. - For 3D spatial audio:
AudioSource.spatialBlend = 1for fully positional. UseLogarithmicrolloff for physics-accurate attenuation;Linearfor predictable gameplay audio. SetminDistanceandmaxDistanceper source — a guard post sound withmaxDistance = 20never interferes with sources 200 m away. - Pool
AudioSourcecomponents — return to pool whenAudioSource.isPlaying == false.
Lesser-known tricks:
- Steam Audio provides physics-based occlusion (raycast from listener to source), early reflections, and reverb. Integration: add
SteamAudioSourcecomponent, check Occlusion. Export scene geometry withSteamAudioGeometry. Parametric reverb is least expensive; True Audio Next uses GPU acceleration. Steam Audio Unity Guide AudioSettings.GetConfiguration()/AudioSettings.Reset()lets you change DSP buffer size at runtime — lower buffer size = lower latency at cost of higher CPU. Expose in graphics options.- ADPCM is underused. For any short, high-frequency sound (footsteps, bullet impacts, UI clicks), ADPCM gives 3.5× compression at negligible CPU cost — better than Vorbis for short clips due to lower decode overhead.
AudioSource.Pause()+AudioSource.UnPause()is more efficient thanStop()+Play()when you expect to resume the same clip position —Stop()resets clip position and may trigger a new seek on streaming clips.
Tools and profiling for this role:
- Profiler → Audio module → DSP CPU, voice count.
- Memory Profiler → Audio category.
- AudioStats window (Window → Analysis → Audio Stats) for real-time voice visualization.
Cheat sheet:
| Setting | Memory | CPU | Recommendation |
|---|---|---|---|
| Music: Decompress on Load | Very High | Low | Never — use Streaming |
| SFX: Compressed in Memory | Low | Medium | Standard for medium clips |
| Short SFX: Decompress on Load | Medium | Very Low | Acceptable for <1 s clips |
| Force to Mono (3D sources) | Halves memory | None | Always enable |
| Max Voices | N/A | Scales linearly | Set priority; disable silent sources |
Animations
Animation is a consistent mid-range CPU cost that compounds quickly in scenes with many characters. Configuration choices here are permanent — wrong settings at import time persist through the game's lifetime.
System comparison:
| System | When to use | Performance profile |
|---|---|---|
| Legacy Animation | Avoid in new projects | Fast for simple single-clip (no blend overhead) |
| Animator / Mecanim | Characters with blending | Medium-high CPU; scales with layer and bone count |
| Playable API (2017+) | Code-driven, no state machine | Low overhead; full control |
| Animation Rigging (2019+) | Runtime IK, aim, multi-position | Per-constraint cost; runs after Animator |
| DOTS Animation (experimental) | 1000+ uniform-crowd characters | Parallel Burst jobs; steep learning curve |
Common pitfalls:
- [All versions]
Animator.cullingMode = AlwaysAnimate(default) — evaluates full state machine for off-screen characters. Unity Manual – Animation Performance - [All versions] Humanoid rig when Generic would suffice — Humanoid mode is 2–2.5× slower on platforms without NEON SIMD due to Avatar retargeting math.
- [All versions]
Animator.SetFloat("ParameterName", val)with string in Update — internal dictionary lookup per call. - [All versions] Scale curves on clips for characters that never scale — more expensive than translation/rotation curves.
- [All versions]
Animator.Rebind()called in the update loop — rebuilds the entire Animator state, very expensive. - [2020-2022 LTS]
com.unity.animation(DOTS Animation) used for small numbers of characters where the complexity overhead is not offset by performance gains.
Recommended practices:
- Set culling mode explicitly:
// Background NPCs — stop everything when off-screen
_animator.cullingMode = AnimatorCullingMode.CullCompletely;
// with:
skinnedMeshRenderer.updateWhenOffscreen = false;
// Critical story characters — maintain state machine, skip IK
_animator.cullingMode = AnimatorCullingMode.CullUpdateTransforms;
- Cache Animator parameter hashes at initialization:
private static readonly int SpeedHash = Animator.StringToHash("Speed");
private static readonly int IsGroundedHash = Animator.StringToHash("IsGrounded");
void Update()
{
_animator.SetFloat(SpeedHash, _velocity.magnitude);
_animator.SetBool(IsGroundedHash, _isGrounded);
}
Unity Manual – Animation Performance
- Enable GPU Skinning in Player Settings → Other Settings. Offloads vertex skinning to a Compute Shader pass. DOUBLE Unity Animation Performance
- Use Generic rig instead of Humanoid unless cross-rig retargeting or Humanoid IK is required.
- Enable Optimize Game Objects in rig import for crowds — collapses bone hierarchy, removes per-bone GameObjects, saves transform update overhead.
- Use the Playable API for code-driven animation without state machine overhead:
var graph = PlayableGraph.Create("MyGraph");
var clipPlayable = AnimationClipPlayable.Create(graph, clip);
var output = AnimationPlayableOutput.Create(graph, "Output", _animator);
output.SetSourcePlayable(clipPlayable);
graph.Play();
- Reduce Blend Weights (
Quality.blendWeights) to 2 for LOD1 and beyond. Four bones is the humanoid default; two is usually indistinguishable at medium distance. - Strip scale curves in import settings for characters that never scale — use an
AssetPostprocessorto automate this. - Set
RigLayer.weight = 0on Animation Rigging constraints when the rig is inactive (e.g., weapon-aim rig when unarmed).
Lesser-known tricks:
- [Unity 6] Batched GPU Skinning reduces the number of separate Compute dispatches for groups of identically-configured skinned meshes — a significant improvement for crowd scenarios.
- Avatar Mask on Animator layers constrains which bones the layer affects — an upper-body aim layer with a full-body mask wastes evaluation time on lower-body bones. Always assign precise masks to additive layers.
- Animation Compression in clip import settings (Optimal, Reduced Keyframes, Custom) affects both storage size and runtime evaluation cost. Optimal is the recommended default; Custom allows per-curve compression for precision-sensitive animations (facial).
- The Playable API cost is proportional to clips actually playing, not state machine complexity — prefer it over Animator Controller for dialogue systems, procedural animation, or cinematics.
- DOTS Animation (experimental) has no built-in state machine; blend graph nodes must be authored in the data-flow graph API. Not production-ready for diverse animation needs, but valid for uniform crowd scenarios (sports audiences, pedestrians).
Tools and profiling for this role:
- Profiler → CPU → filter for
Animator.Update,AnimationUpdate,PreLateUpdate.DirectorUpdateAnimationBegin. - Check
Animatorscount in Rendering Profiler module. - Frame Debugger → SkinnedMesh section for GPU Skinning dispatch count.
Cheat sheet:
| Setting | Default | Recommendation |
|---|---|---|
cullingMode | AlwaysAnimate | CullCompletely for background |
| Rig type | Humanoid | Generic unless retargeting needed |
| GPU Skinning | On (where supported) | On always |
| Blend Weights | 4 | 2 for LOD1+; reduce per-LOD |
| Optimize Game Objects | Off | On for crowds |
| Animation Compression | Optimal | Optimal default |
Physics
Unity uses PhysX (NVIDIA's physics library) for its built-in 3D physics. Physics is one of the most reliable sources of CPU budget overruns in games that grow organically — it starts fast and gets expensive as designers add more interactive objects.
Common pitfalls:
- [All versions] Running non-physics logic in
FixedUpdate—FixedUpdatemay be called multiple times per frame if the game is running slowly, creating a "death spiral" where physics work compounds into a freeze. Strictly reserveFixedUpdateforRigidbodyforces and physics queries. - [All versions] Using
Physics.RaycastwithRaycastHitallocation inside a hot loop whenPhysics.RaycastNonAllocexists. - [All versions] Enabling Mesh Colliders on high-polygon meshes for non-convex shapes — non-convex Mesh Colliders are expensive to query and cannot participate in dynamic rigidbody physics. Use compound primitive colliders instead.
- [All versions]
Physics.autoSyncTransforms = true(default) syncing transforms on every write — in physics-heavy scenes with many repositioned objects this produces redundant synchronization every frame. Unity physics auto-sync transforms - [All versions] Default physics layer collision matrix including all pairs — unchecking irrelevant layer-layer interactions in Project Settings — Physics — Layer Collision Matrix eliminates broad-phase query cost.
- [All versions] Fixed timestep too small — default 0.02 s (50 Hz) is fine for most games but ambitious defaults (0.01 s = 100 Hz) double physics CPU cost for marginal benefit in non-competitive games.
- [2020-2022 LTS] Unity Physics (DOTS) used without Burst compilation — the entire value proposition of DOTS Physics is that the simulation runs in Burst-compiled parallel jobs; managed-only DOTS Physics is slower than PhysX.
Recommended practices:
- Use
Physics.RaycastNonAlloc(ray, results, distance, layerMask)instead ofPhysics.Raycastin hot paths:
// Pre-allocate once
private readonly RaycastHit[] _hits = new RaycastHit[10];
void Update()
{
int count = Physics.RaycastNonAlloc(transform.position,
Vector3.down, _hits, 5f, groundMask);
if (count > 0) HandleGroundHit(_hits[0]);
}
Similarly, Physics.OverlapSphereNonAlloc, Physics.BoxCastNonAlloc and their 2D equivalents all have NonAlloc variants.
- Disable
Physics.autoSyncTransformsand callPhysics.SyncTransforms()manually before batches of queries:
void Start() => Physics.autoSyncTransforms = false;
void UpdatePhysicsQueries()
{
// Move objects first
MoveAllObjects();
// Sync once for all subsequent queries
Physics.SyncTransforms();
PerformAllRaycasts();
}
TheGameDev.Guru — Physics autoSyncTransforms
- Open Project Settings → Physics → Layer Collision Matrix and uncheck every layer pair that will never interact. The broad-phase AABB tree queries this matrix to eliminate pairs early; fewer enabled pairs = faster broad-phase.
- Use Compound Colliders (multiple primitive Colliders on child GameObjects with one Rigidbody on the parent) instead of non-convex Mesh Colliders for complex shapes. Spheres, capsules, and boxes are far cheaper to query than arbitrary meshes.
- Increase
Time.fixedDeltaTimeto 0.025 s (40 Hz) or 0.033 s (30 Hz) for non-competitive games that do not require high-fidelity collision response. The visual impact is minimal for most gameplay. - Use sleep thresholds —
Rigidbody.sleepThreshold(default 0.005) determines when a Rigidbody stops being simulated. Objects that settle but have a low sleep threshold keep consuming physics budget. Increase the threshold for objects that should rest quickly (furniture, props). - Avoid
Rigidbody.MovePosition/MoveRotationinsideUpdate()— these are physics operations that should be called inFixedUpdate. Called fromUpdate, they can produce one-frame lag artifacts and incorrect physics interpolation. - Enable Interpolation on player Rigidbodies:
Rigidbody.interpolation = RigidbodyInterpolation.Interpolate. This smooths the visual position between fixed timesteps for fast-moving objects without changing the simulation rate. - For queries that run every frame on many agents (pathfinding, line-of-sight), batch the queries using Unity's
Physics.Raycastinside a Job viaRaycastCommand:
// RaycastCommand batches many raycasts as a Job
var commands = new NativeArray<RaycastCommand>(agentCount, Allocator.TempJob);
var results = new NativeArray<RaycastHit>(agentCount, Allocator.TempJob);
for (int i = 0; i < agentCount; i++)
commands[i] = new RaycastCommand(agents[i].position, Vector3.down, 5f);
JobHandle handle = RaycastCommand.ScheduleBatch(commands, results, 32);
handle.Complete();
for (int i = 0; i < agentCount; i++)
if (results[i].collider != null) HandleHit(i, results[i]);
commands.Dispose();
results.Dispose();
RaycastCommand runs the queries on worker threads, parallelizing line-of-sight for large numbers of agents.
- 2D Physics (
Physics2D) has a separateautoSimulationtoggle (Physics2D.autoSimulation = false) enabling manual stepping viaPhysics2D.Simulate(Time.fixedDeltaTime)inside a custom loop — useful for turn-based games or deterministic replay.
Lesser-known tricks:
Physics.IgnoreLayerCollision(layerA, layerB, true)at runtime to temporarily disable physics between two layers (e.g., player layer and collectibles during an invincibility phase) — cheaper than destroying and rebuilding the collision matrix.Rigidbody.detectCollisions = falsedisables collision detection for a specific Rigidbody without disabling the GameObject. Use for pooled projectiles on return to pool — avoids triggeringOnTriggerExiton the return path.- Physics Debugger (Window → Analysis → Physics Debugger) visualizes the active colliders, Rigidbodies in the sleep/awake state, and contact points. Essential for diagnosing "why is this object not sleeping?" and confirming compound collider setup.
Rigidbody.WakeUp()andRigidbody.Sleep()allow manual sleep/wake control. Manually sleeping a Rigidbody that will not move for several frames (an inactive trap, a dropped item) removes it from the physics simulation entirely.Physics.queriesHitBackfaces = false(default) means raycasts only detect front faces. Setting it true doubles the potential hit count per query. Keep it false unless your game design specifically requires back-face hits.- DOTS physics (
Unity Physics) is deterministic by design — the exact same inputs produce the exact same outputs regardless of platform. Built-in PhysX is deterministic on the same hardware/driver combination but not across different systems. If you need cross-platform determinism for replays or multiplayer, DOTS Physics is the correct choice. Unity Physics vs Havok Physics
Tools and profiling for this role:
- Profiler → Physics module:
Physics.Processing(main simulation),Physics.FetchResults(result sync),Physics.Interpolation(position smoothing cost). - Physics Debugger for visual collider audit.
stat physicsin the Editor console for real-time broad/narrow phase counts.- Profiler → CPU Timeline:
FixedUpdate.PhysicsFixedUpdategives the fixed-step budget. If this exceeds the fixed timestep, physics is behind and the death spiral begins.
Cheat sheet:
| Setting | Default | Performance recommendation |
|---|---|---|
Time.fixedDeltaTime | 0.02 (50 Hz) | 0.025–0.033 for non-competitive |
Physics.autoSyncTransforms | true | false (sync manually) |
Rigidbody.sleepThreshold | 0.005 | Increase for static props |
Rigidbody.interpolation | None | Interpolate for player/camera targets |
| Mesh Collider (non-convex) | Allowed | Avoid; use compound primitives |
| Layer Collision Matrix | All pairs | Disable irrelevant pairs |
RaycastNonAlloc | Not default | Use in all hot-path queries |
UI (UGUI and UI Toolkit)
Unity's UI system is the most commonly misunderstood performance area. Canvas batching rules are non-intuitive and the default component settings are wrong for performance.
Common pitfalls:
- [All versions] Every
ImageandRawImagehas Raycast Target = true by default. The Graphic Raycaster iterates every enabled Raycast Target on input. 200 images = 200 intersection tests per frame. - [All versions] The same applies to TextMeshPro text objects — they are also created with Raycast Target enabled by default. You can change this default for all newly created TMP text objects in the TMP Settings configuration file. (contributed by Michał Szymerski)
- [All versions] Single Canvas with thousands of mixed static and animating elements — any change to any element rebuilds the entire Canvas.
- [All versions] Layout Group on high-frequency dynamic elements — each Layout Element dirty triggers
GetComponentcalls walking up the transform hierarchy. - [All versions]
SetActive(true/false)on a Canvas with many children — triggers full rebuild on activate. - [All versions] Using legacy
Textcomponent — sub-pixel rasterization, frequent atlas rebuilds. Use TextMeshPro exclusively. - [2020-2022 LTS] World Space Canvas without necessity — full per-frame rasterization, affected by scene lighting, most expensive Render Mode.
Recommended practices:
- Split static and dynamic UI into separate Canvases:
Root Canvas
├── Static Canvas (backgrounds, labels, non-animated icons)
│ → Batches once on load, never rebuilds
└── Dynamic Canvas (health bars, timers, score, animated elements)
→ Rebuilds frequently but only contains changing elements
└── Sub-Canvas per high-frequency element if needed
- Disable Raycast Target on every non-interactive element (decorative images, backgrounds, text labels that aren't buttons). This is a systematic change — do it for every Image component in the project.
- Use Screen Space - Overlay Render Mode as the default (cheapest; no camera involvement). Use Screen Space - Camera only when depth effects are required. World Space only when diegetic UI is a design requirement.
- Use TextMeshPro for all text. SDF rendering, resolution-independent, GPU-cheap, supports rich text. Unity Manual – UI System Compare
- Pack UI icons, button states, and small decorative images into Sprite Atlases — elements from the same atlas share a material and batch into one draw call. However, remember to always leave the source sprites without compression and set the compression on the Sprite Atlas only — otherwise you will end up with double-compressed, messed up sprites. (contributed by Michał Szymerski)
- Avoid baking layouts in Layout Groups on high-frequency dynamic content. Instead, anchor positions and offsets where possible — they are purely a transform property, not a layout calculation.
- For UI Toolkit (production-ready Unity 2022 LTS+), animate via USS transitions on
translate,scale,rotate— these run on the GPU viaDynamicTransformusage hint and do not trigger layout recalculation. Never animatewidth,height,top, orleft— those trigger expensive layout recalcs. Unity Manual – Optimizing UI Toolkit - UI Toolkit batches via an "uber shader" supporting up to 8 textures per batch. Exceeding 8 unique textures in one batch breaks batching.
Performance comparison (1000 interactive elements, Unity 2022.3 LTS): Angry Shark Studio – UI Toolkit vs UGUI 2025
| Metric | UGUI | UI Toolkit |
|---|---|---|
| Draw Calls | 45 | 5 |
| CPU Frame Time | 12.5 ms | 4.2 ms |
| Memory Usage | 125 MB | 48 MB |
| Instantiation (100 items) | 85 ms | 15 ms |
Lesser-known tricks:
- Sub-Canvases isolate their contents from parent Canvas rebuilds — but the system does not batch across separate Canvases, so excessive splitting increases draw calls. Target 2–4 Canvases per UI screen. Unity Learn – Optimizing Unity UI
Canvas.enabled = false/= trueis faster thangameObject.SetActive(false/true)for temporarily hiding a Canvas — it avoids tearing down and rebuilding the Canvas children's state.- For high-frequency numeric updates (score, health, ammo), update the text string only when the value changes — not every frame. Store the last-rendered value and skip the text assignment if unchanged.
- Particle effects inside a Canvas with World Space Render Mode cause the Canvas to treat each particle as a UI element. Don't mix Particle Systems with World Space Canvases without profiling the overhead first.
CanvasGroup.alpha = 0hides UI visually but does not disable Raycast Targets — useCanvasGroup.blocksRaycasts = falseas well, or disable the Canvas.
Tools and profiling for this role:
- Profiler → CPU →
Canvas.BuildBatch,Canvas.SendWillRenderCanvases— these fire every time a Canvas rebuilds. - Profiler → GPU → UI section.
- Frame Debugger → UI category for draw call count and Canvas breakdown.
Cheat sheet:
| Setting/Pattern | Cost | Recommendation |
|---|---|---|
| Raycast Target default | High (many tests) | Disable on all non-interactive |
| Single large Canvas | High (full rebuild) | Split static/dynamic |
| Layout Groups on dynamic | Medium | Bake into anchors |
| World Space Render Mode | High | Only for diegetic UI |
| Legacy Text | Medium (atlas rebuilds) | Always use TextMeshPro |
Networking and Multiplayer
Networking optimization is as much about architecture decisions as about code — the right framework choice for game scale matters more than micro-optimizations.
Framework comparison:
| Framework | Type | Scale | Strength | Limitation |
|---|---|---|---|---|
| Netcode for GameObjects (NGO) [2022+] | First-party Unity | 2–10 players | Unity integration, server-authoritative | Limited high-scale |
| Netcode for Entities [2022+] | First-party, DOTS | High | Burst jobs, deterministic | Requires full DOTS |
| Mirror | Community OSS | Small–large | Flexible transports | No cloud infra |
| Photon Fusion / PUN | Commercial SaaS | Large, global | Cloud, lag comp, rollback | Cost scales with CCU |
| Steam P2P / Steamworks | P2P relay | Small sessions | No server cost | Steam ecosystem only |
Unity NetCode for GameObjects tutorial
Common pitfalls:
- [2020-2022 LTS] Mutating
NetworkVariableevery frame — only changed values are sent, butIsDirtychecking still has overhead. Mutate only on actual game state change. - [2020-2022 LTS]
ServerRpccalls every frame — batching is automatic per tick but the RPC submission overhead compounds. - [2020-2022 LTS] Bandwidth-heavy state synchronization without visibility culling —
NetworkObject.CheckObjectVisibilityshould exclude objects not relevant to a given client. - [All versions] Tick rate mismatch — if client update rate < server tick rate, bandwidth is wasted with no benefit.
- [All versions] No lag compensation for competitive hit detection — players see positions 50–100 ms behind server state; shots never connect where aimed.
Recommended practices:
-
Set tick rate via
NetworkManager.TickRate:- 30 Hz: Turn-based, casual, co-op
- 60 Hz: Action games, platformers
- 128 Hz: Competitive FPS (Photon Fusion) Unity Manual – Tick and Update Rates
-
Batch input into structs sent once per tick, not
ServerRpccalls per input event. -
Implement
NetworkObject.CheckObjectVisibilitydelegate to cull NetworkObjects from clients based on distance — objects not relevant to a client should not exist in their world. -
Quantize position floats to 16-bit integers for low-bandwidth transmission. NGO's
NetworkTransformexposes interpolation and compression settings. -
For lag compensation in NGO: record
NetworkTransformpositions with timestamps in a circular buffer. OnCmdFire, binary-search forserverTime - clientRTT/2, perform hit test against rewound state. Mirror – Lag Compensation -
Use Unity Relay (Unity Gaming Services) for small-scale P2P games without dedicated server infrastructure. Limited throughput per relay — not suitable for >30 players or bandwidth-heavy simulations.
-
For competitive or >30-player games: Unity Dedicated Server build target (headless, Unity 2021+) provides full control and arbitrary player count.
-
Reference Boss Room (Unity's official NGO sample, 8-player co-op dungeon) for production NGO patterns: lobby flow, connection approval, in-game state machine, NetworkObject spawning. Boss Room
Lesser-known tricks:
- NGO's
NetworkVariablechanges within one tick are batched into a singleNetworkVariableDeltaMessage— you do not need to manually batch variable changes, just keep them within the same tick. NGO GitHub tick delivery - The distinction between server-authoritative (server owns positions, clients send inputs) and client-authoritative (clients own positions) is architectural. Server-authoritative is harder to implement but not cheatable. Use client-authoritative only for non-competitive internal tools or hotseat games.
- For Netcode for Entities: the entire entity simulation is deterministic and Burst-compiled, enabling efficient delta compression by sending only structural changes, not full position snapshots.
- Photon Fusion's rollback netcode model maintains a full copy of game state per tick for client-side prediction and server reconciliation — this requires all game-state types to be blittable. Plan your state structures before adopting Fusion.
Tools and profiling for this role:
- Profiler → Network module (NGO specific).
- Wireshark for raw packet inspection and bandwidth measurement.
- NGO's
NetworkStatsMonoBehaviour for in-game bandwidth display. - Unity Multiplayer Center (Package Manager → Multiplayer Center) for architecture guidance.
Cheat sheet:
| Parameter | Recommended | Notes |
|---|---|---|
| Tick Rate (casual) | 30 Hz | Turn-based, co-op |
| Tick Rate (action) | 60 Hz | Platformers, action |
| Tick Rate (competitive) | 128 Hz | FPS, Photon Fusion |
NetworkVariable mutation | Only on change | Not in Update() |
| Object visibility | Cull at distance | CheckObjectVisibility |
QA / Build / Production
Build configuration and QA workflows are where the gap between development performance and shipping performance is closed.
Common pitfalls:
- [All versions] Profiling in a Development Build and drawing conclusions — Development Builds disable IL2CPP code stripping and shader stripping, slightly inflate performance cost.
- [All versions] Shipping with Mono backend — always ship IL2CPP.
- [All versions] No
asmdeffiles — every code change triggers a full project recompile. - [2020-2022 LTS] Missing
link.xmlwhen using aggressive stripping with reflection-heavy code — silent runtime failures. - [All versions]
Addressables.ReleaseAsset/ReleaseInstancenot called — most common Addressables memory leak. - [2020-2022 LTS] Single asset bundle scheme — granularity is wrong; impossible to unload individual assets without analyzing dependencies.
Recommended practices:
- Build configurations:
| Configuration | When to use |
|---|---|
| Development Build | All development; enables Profiler connection |
| Script Debugging | Managed debugger; significant overhead; use only when debugging |
| Autoconnect Profiler | Attaches Profiler on startup; use for automated performance runs |
Never ship a Development Build. Always profile against a non-Development build for final performance metrics.
- IL2CPP settings for shipping:
- Scripting Backend: IL2CPP
- API Compatibility: .NET Standard 2.1 (smaller BCL, better stripping)
- IL2CPP Code Generation (Unity 2021+): "Faster runtime" for shipping builds — generates fully expanded generic code, faster execution, larger binary Unity Manual – IL2CPP scripting backend
- Managed Stripping Level: Medium or High (test thoroughly at High); preserve reflection-dependent code in
link.xml
<linker>
<assembly fullname="Assembly-CSharp">
<type fullname="MyGame.SaveData" preserve="all"/>
</assembly>
<assembly fullname="Newtonsoft.Json" preserve="all"/>
</linker>
-
Add Assembly Definition files per subsystem (Input, AI, UI, Networking). Compile savings of 60–80% on large projects are routinely reported. Wrap third-party/Asset Store code first (rarely changes → compiles once). Runtime vs Editor assembly split prevents editor-only changes from recompiling runtime. Use Compilation Visualizer to audit dependency chains. Coffee Brain Games – asmdef
-
Addressables patterns:
- Pack Separately vs Pack Together — group assets that load/unload together into the same bundle. Fine-grained bundles enable precise unloading but increase metadata overhead.
- Run Check Duplicate Bundle Dependencies in Analyze window — shared textures pulled into multiple bundles create duplicate assets in memory.
- Always call
Addressables.ReleaseAsset/ReleaseInstance— missing calls are the most common Addressables memory leak. Unity Blog – Saving memory with Addressables - You can track the allocation state of Addressables in the Profiler — add the Addressable Assets module to the Profiler view (Profiler Modules dropdown). (contributed by Michał Szymerski)
-
Unity Test Runner for automated performance regression:
- Edit Mode tests: pure logic, math, serialization (no Play Mode needed)
- Play Mode tests: full MonoBehaviour lifecycle
- Performance Testing Extension (
com.unity.test-framework.performance):Measure.MethodandMeasure.Framesfor percentile data; integrates with CI to flag regressions
-
CI options:
| Option | Notes |
|---|---|
| Unity Cloud Build | Managed CI; tight Unity integration; limited environment control |
| GitHub Actions | Free tier with self-hosted runners; supports Unity via game-ci/unity-builder |
| TeamCity / Jenkins | Full control; preferred for large studios; license seat management |
- Use
BuildPipeline.BuildPlayerscripted builds andUnityEditor.Build.Reporting.BuildReportAPI for automated build-size tracking in CI. Fail the build if compressed size exceeds threshold. - Enable Domain Reload and Scene Reload disabling immediately at project start — not after the project becomes large. The payoff is immediate and the migration cost is front-loaded.
Lesser-known tricks:
- Build Profiles (Unity 2023.1+): named build configurations with per-profile Player Settings overrides. Eliminate manual reconfiguration when switching between PC/console/QA builds.
Scripting Define Symbolsin Player Settings gate debug, profiling, and feature code at compile time — zero runtime cost in production builds. UseDEVELOPMENT_BUILDas a built-in gate.Application.BuildGUIDis a unique string per build — store it in the game version overlay for QA to identify exact builds without guessing.- The Build Report
Library/LastBuild.buildreportis a Unity ScriptableObject — readable via C# in Editor scripts for CI asset-size tracking without installing the Build Report Inspector package.
Tools and profiling for this role:
- Build Report Inspector for post-build asset size analysis.
- Profile Analyzer for quantitative regression comparison.
- Compilation Visualizer for asmdef dependency audit.
- Unity Test Runner + Performance Testing Extension for automated performance gates.
Cheat sheet:
| Setting | Development | Shipping |
|---|---|---|
| Scripting Backend | Mono | IL2CPP |
| IL2CPP Code Gen | Faster builds | Faster runtime |
| API Compatibility | .NET Standard 2.1 | .NET Standard 2.1 |
| Stripping Level | Low | Medium–High |
| Domain Reload | Disabled | N/A |
| Development Build | On | Off |
Tech Art
Tech artists live at the intersection of rendering and engineering, responsible for the systems that tie art into the engine pipeline without breaking performance.
Common pitfalls:
- [2020-2022 LTS]
MaterialPropertyBlockused unintentionally killing SRP Batcher compatibility — any renderer with an MPB falls back to GPU Instancing or unoptimized draw calls. - [All versions] Shader Graph multi-compile keyword spam — every Custom Function Node with conditional paths can double variant counts if not carefully scoped.
- [Unity 6+] Not migrating URP Renderer Features to the Render Graph API — all
ScriptableRenderPass.Executeoverrides must useRecordRenderGraphin Unity 6, with the old path deprecated. - [Unity 6+] Enabling GPU Resident Drawer without also enabling Forward+ rendering path — GRD requires Forward+.
- [All versions] HDR render target enabled when Bloom is not used — R16G16B16A16_SFloat uses 8 bytes/pixel vs 4 for LDR. Disable HDR in URP Asset → Quality → HDR if Bloom is not active.
- [2020-2022 LTS] Camera Stacking with post-processing on each camera in the stack — post-processing should apply only on the last camera to avoid duplicate effect passes.
Recommended practices:
- Audit SRP Batcher compatibility via Frame Debugger: expand the SRP Batcher node. "Batch reason: different shader" means even identical materials on different base shaders cannot batch. "Different material properties" means
MaterialPropertyBlockor mismatched CBUFFER content. - [Unity 6+] Enable GPU Resident Drawer in URP scenes with many repeated mesh-material pairs:
- Universal Renderer asset → GPU Resident Drawer → Instanced Drawing
- Requires: Forward+ rendering path, compute shader support, SRP Batcher enabled, Static Batching disabled
- Also enable GPU Occlusion Culling for matching depth-buffer occlusion at no bake cost Unity GPU Resident Drawer docs
- [Unity 6+] Migrate Renderer Features to Render Graph API:
// Unity 6+ — Render Graph API (required)
public override void RecordRenderGraph(RenderGraph renderGraph,
ContextContainer frameData)
{
using (var builder = renderGraph.AddRasterRenderPass<PassData>(
"MyPass", out var passData))
{
// declare resource usage, not execution
builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
ExecutePass(data, context));
}
}
Write a render pass using Render Graph
- [Unity 6+] Enable STP (Spatial-Temporal Post-Processing) for GPU-bound scenes:
- URP Asset → Quality → Upscaling Filter → Spatial-Temporal Post-Processing
- 0.7–0.8 Render Scale with STP can match 1.0 native quality at significantly lower GPU cost Unity STP docs
- URP Renderer Feature injection points (use the most downstream that works):
BeforeRenderingOpaques— for effects that modify depth before geometry passAfterRenderingOpaques— for screen-space effects using opaque depthAfterRenderingTransparents— for effects needing full sceneBeforeRenderingPostProcessing— for effects that post-processing must read
- HDRP Custom Pass injection points:
BeforePreRefraction,BeforeTransparent,BeforePostProcess,AfterPostProcess. Supports fullscreen, object, and quad injection types. - Choose Render Texture format by precision need:
R8_UNorm— 1 byte/px, masks, stencil-like effectsR8G8_UNorm— 2 bytes/px, packed normal XY or two masksR8G8B8A8_UNorm— 4 bytes/px, standard LDR colorR16G16B16A16_SFloat— 8 bytes/px, HDR, physically-based values
- For HDRP Decal Projectors: each active projector adds a draw call and samples the decal atlas. Set
Max Decal on Screenin HDRP Asset; bake static decals into albedo lightmaps where possible. Clustered decal rendering available from HDRP 2022+. HDRP Local Volumetric Fog - DLSS/FSR/XeSS status:
- DLSS: HDRP native (Unity 2021.2+); URP via NVIDIA DLSS package. Requires RTX hardware, DX12/Vulkan.
- FSR: AMD, hardware-agnostic; integrated via
com.unity.render-pipelines.coreTAA in URP/HDRP from Unity 2022+. - XeSS: Intel, DX12 only; open-source plugin. Unity STP docs
Lesser-known tricks:
- Custom Render Textures support self-updating procedural textures with configurable update modes (on demand, per frame, real-time) — useful for animated noise, fluid simulations, texture projections without requiring a camera.
- Disable HDR when Bloom is inactive. LDR render targets use half the memory and bandwidth. Rendering tricks research
- Render Scale + STP:
0.7 scale + STPis often visually indistinguishable from1.0 nativeat a meaningful GPU cost reduction. Run a blind comparison before publishing any marketing claims. - Camera Stacking in URP: overlay cameras always render in Forward path even if the Base Camera uses Deferred. Culling optimizations within a single camera do not apply across stack boundaries.
Shader.WarmupAllShaders()compiles every variant — thorough but slow. PreferShaderVariantCollection.WarmUp()with a curated collection for controlled warm-up scope. Unity 6's Shader Pipeline Library persists compiled variants across sessions, eliminating first-frame stutter on repeat sessions.
Tools and profiling for this role:
- Frame Debugger — primary tool for SRP Batcher analysis, Render Graph pass names (Unity 6), render order.
- RenderDoc — visual debugging, texture/buffer inspection, draw call state.
- NVIDIA Nsight Graphics — warp utilization, shader occupancy for GPU-bound shaders.
- Rendering Debugger (URP/HDRP) — overdraw, Material Validation, Light Overlaps.
Cheat sheet:
| Feature | Min version | Enable via |
|---|---|---|
| SRP Batcher | URP 2019.3+ | On by default in URP |
| GPU Resident Drawer | Unity 6, URP | Universal Renderer asset → Instanced Drawing |
| Render Graph | Unity 6 mandatory | All new Renderer Features |
| STP | Unity 6 | URP Asset → Upscaling Filter → STP |
| APV | Unity 6 | Light Probe System → APV |
| Forward+ | URP 2021.2+ | Universal Renderer → Rendering Path |
Last Resort Methods
When profiling reveals systemic issues too large to fix before a deadline, these are valid temporary measures with known quality costs. They are not substitutes for proper optimization — they buy time.
Quality Settings drop:
QualitySettings.SetQualityLevel(1)at runtime to switch to a lower tier mid-session for hardware that misses frame budget.- Expose as a user-facing setting in the options menu. At minimum: Low, Medium, High. Low should target 1080p/30 FPS on 5-year-old integrated graphics; High should target 1440p/60 FPS on mid-range discrete.
Render Scale:
- URP Asset → Quality → Render Scale. Reduce to 0.7–0.75 as a GPU-bound relief valve. Pair with STP (Unity 6+) for quality recovery.
- Expose Render Scale as an advanced graphics option — some players prefer 0.85 at 4K over 1.0 at 1080p.
Scalability per-platform quality tier configuration:
[Low Quality Tier]
Shadow Distance: 60
Cascade Count: 1
Shadow Quality: Soft PCF 2x2
MSAA: Off
Anti-Aliasing: FXAA
Render Scale: 0.75
HDR: Off
SSAO: Off
Bloom: Off
Depth of Field: Off
[Medium Tier]
Shadow Distance: 100
Cascade Count: 2
MSAA: 2x
Anti-Aliasing: TAA
Render Scale: 0.85
HDR: On
SSAO: Low (4 samples)
Bloom: On
[High Tier]
Shadow Distance: 150
Cascade Count: 3–4
MSAA: Off (use TAA/STP)
Render Scale: 1.0
HDR: On
SSAO: Medium (8 samples)
Bloom: On, DoF: On
[Ultra Tier]
Shadow Distance: 200+
STP/DLSS/FSR: On
Cascade Count: 4
All effects: On
Render Scale: 1.0 (or >1.0 with DLSS Quality)
Texture quality downgrade:
QualitySettings.globalTextureMipmapLimit = 1— forces all textures to render at half resolution globally. A blunt instrument; use per-texture mip streaming first.
Physics fixed timestep:
- Increase
Time.fixedDeltaTimefrom 0.02 (50 Hz) to 0.033 (30 Hz) for CPU-bound physics. Lower fidelity collision response but significant CPU save in physics-heavy scenes.
Profiling Workflow Reference
This section is a condensed workflow guide for the most common profiling sessions. It complements the Tools section with ordered process steps rather than feature descriptions.
Session 1: Identifying the Bottleneck Type
The fastest diagnostic session is a 5-minute triage. Run it first, before opening any specialization section.
- Build a Development Build with Autoconnect Profiler enabled. Never profile in Editor Play mode for shipping decisions.
- Connect the Profiler and navigate to the worst-case in-game area (highest density, most active enemies, peak VFX).
- Capture 60–100 frames. Open Profile Analyzer and note the median frame time and 99th percentile.
- Open a median frame in CPU Timeline. Identify the widest bar on the main thread:
BehaviourUpdatedominant → C# gameplay code bottleneck. Go to Code & Mechanics section.Camera.Render/PostLateUpdate.FinishFrameRenderingdominant → rendering. Check GPU module.Physics.Processingdominant → physics. Go to Physics section.Gfx.WaitForPresentOnGfxThreadlarge → GPU-bound. Look at GPU module breakdown.
- Check the Rendering module for draw call count and SetPass count:
- SetPass calls ≈ draw calls: SRP Batcher not working. Open Frame Debugger.
- SetPass calls << draw calls: SRP Batcher working but many objects. Consider GPU Resident Drawer (Unity 6+).
- Both low but GPU-bound: fill-rate problem (overdraw, post-processing, shadow maps). Unity profiling best practices
Session 2: GC Allocation Hunting
GC spikes appear as periodic frame-time bumps that don't correspond to visible scene events. Run this session when frame time is irregular.
- In the CPU Profiler Hierarchy, enable the GC Alloc column. Sort by GC Alloc descending.
- Enable Allocation Call Stacks in the Profiler toolbar (gear icon). This traces each
GC.Allocback to its source line in a subsequent capture. - Re-capture with call stacks enabled. Find the top allocators in
BehaviourUpdate. - Common hotspots:
Debug.Log(always allocates a string), LINQ expressions,Camera.main,new WaitForSeconds(),GetComponent<T>()results not cached, coroutines allocating state machines. - After fixing each allocator, re-run Profile Analyzer Compare mode with the before/after
.pdatafiles to confirm the GC Alloc column dropped.
Incremental GC note: With Incremental GC on, spikes are spread across frames. Sort the Hierarchy by Total (not Self) and look for GC.Collect entries to find residual full-collection events. If GC.Collect still appears regularly, total allocation rate is too high for incremental collection to keep up. Unity incremental GC docs
Session 3: GPU Bottleneck Isolation
- In the GPU Profiler module, identify which category is largest: Geometry, Shadows, Lighting, Post-Processing.
- For Shadows: reduce
QualitySettings.shadowDistanceby 20% increments. Open Frame Debugger to count shadow map render passes (one per shadow-casting light per cascade). - For Geometry: check triangle count in Rendering module. Enable LOD Groups or reduce LOD Bias.
- For Post-Processing: disable effects one by one via Volume overrides in the editor. The one that causes the largest GPU time drop is the primary target.
- For Overdraw: open RenderDoc, inspect the Depth/Color targets for transparent geometry. Or use Rendering Debugger → Lighting → Overdraw view.
- When GPU-bound on PC: consider dropping Render Scale to 0.85 with STP (Unity 6+) or TAA upscaling. A 0.85 render scale reduces pixel count by ~28%, which typically matches a ~25% GPU time reduction for fill-rate-bound passes.
Session 4: First-Frame and Load Spike
First-frame spikes (200+ ms on startup or level load) are distinct from steady-state performance. Diagnose them separately:
- Set
Application.targetFrameRate = -1and disable VSync for the load sequence so the full spike is visible in the Profiler without VSync padding. - Look for
ShaderLab.CreateGpuProgram— shader compilation stutters on first use. Fix withShaderVariantCollection.WarmUp()during a loading screen. - Look for
Loading.LoadObjectFromFileandLoading.AwakeFromLoad— synchronous asset loads. Convert to async withResources.LoadAsyncor Addressables. - Look for
GarbageCollector.CollectIncrementalspikes immediately after load — indicates large number of allocations during scene setup that triggered a collection. Reduce setup allocations. Domain.Reloadin Editor profiling is not present in builds; do not optimize against it for shipping. Disable it in Editor for iteration speed, not for build performance.
Session 5: Memory Audit
Memory problems (gradual leaks, VRAM over-budget) require snapshot-based analysis, not frame-rate profiling.
- Install the Memory Profiler package (
com.unity.memoryprofiler). - Take a snapshot at game start (just after the main menu loads).
- Play for 15–20 minutes. Take a second snapshot.
- Use Diff view to see objects present in snapshot 2 but not snapshot 1. These are potential leaks.
- Common leak sources:
- Addressables assets loaded but
ReleaseAssetnever called — the asset stays resident. - Event handlers (
UnityEvent, C# events) holding references to destroyed objects — the object cannot be collected. - Static dictionaries accumulating entries without eviction.
RenderTexturecreated every frame without explicitRelease().
- Addressables assets loaded but
- Check the Objects and Allocations view for Textures. Find any texture with unexpectedly high memory — common causes: Read/Write Enabled (doubles memory), wrong max size (2048 when 512 would do), wrong format (RGBA32 instead of BC3). Memory Profiler docs
Profiling Checklist (Quick Reference)
| Step | Tool | What you're looking for |
|---|---|---|
| Identify bottleneck type | CPU Profiler Timeline | Widest bar on main thread |
| GC allocation hunt | CPU Hierarchy + Call Stacks | GC Alloc column dominators |
| Draw call analysis | Rendering module + Frame Debugger | SetPass vs batch counts |
| GPU pass breakdown | GPU module + RenderDoc | Geometry/Shadows/PP cost |
| Shader stutter | Frame Debugger + Profiler | ShaderLab.CreateGpuProgram |
| Memory leak | Memory Profiler snapshots | Diff view; growing object types |
| Regression validation | Profile Analyzer Compare | Median frame time before/after |
| Build size audit | Build Report Inspector | Largest assets in build |
Optimization Sweep Steps (Pre-Milestone Checklist)
Run this sweep before every major milestone: Alpha, Beta, Gold Master.
Scene Audit
- Capture 100+ frame Profile Analyzer baseline from Development Build player on minimum-spec hardware
- Identify CPU-bound vs GPU-bound state (check
Gfx.WaitForPresentOnGfxThreadvsBehaviourUpdate) - All major systems have ProfilerMarkers and are visible in Hierarchy
- 99th-percentile frame time within budget (33 ms for 30 FPS, 16.67 ms for 60 FPS)
- No unexpected frame spikes > 2× median in representative gameplay
- Profile Analyzer baseline
.pdatasaved for regression comparison - Draw call count and SetPass count in Rendering module are in expected range
- GC Alloc in steady-state gameplay is 0 bytes per frame (or minimized)
Meshes / Models
- Read/Write Enabled disabled on all imported meshes (unless runtime access required)
- Compress Mesh enabled on all imported meshes
- All meshes ≤ 65K vertices use 16-bit index format
- LOD Groups configured on all hero assets with ≥ 3 LOD levels
- LOD Bias set to target value (0.7–0.85 for most PC games)
- GPU Skinning enabled in Player Settings (Player Settings → Other Settings → GPU Skinning)
- Scale curves stripped from clips for characters that never scale
-
Optimize Game Objectsenabled on all crowd-character rigs - Static Batching disabled if GPU Resident Drawer is enabled (they conflict)
- Meshes used in procedural/runtime updates have
Mesh.MarkDynamic()called
Lighting
- Shadow Distance set to minimum viable range for game type (80–120 m for most PC games)
- Cascade count appropriate for target hardware tier (2–3 for mid-range)
- Realtime Reflection Probes set to
OnDemandrefresh (notEveryFrame) - Lightmaps baked at final quality settings (not Draft/Low quality)
- APV enabled (Unity 6+) or manual probe placement validated and gap-free
- SSAO sample count appropriate per quality tier (4 samples on Low, 8 Medium, 16+ High)
- Mixed Lighting mode is Shadowmask (not Baked Indirect if you have moving objects)
- No lights left on
Realtimemode that should beBakedorMixed - Rendering Layers configured to exclude irrelevant light-mesh pairs
VFX
- All VFX Graph assets have Manual or Recorded bounds (not Automatic)
- All VFX Graph assets have
Culling Flagsset appropriately (stop simulating when invisible) - VFX Graph
Capacityvalues are at minimum required, not set to "just in case" maximums - Particle System Collision modules: World mode replaced with Plane mode or VFX Graph depth collision
- Light module:
Max Light Countset on all Particle Systems using it - Particle System Trail module: trail count capped; width-at-distance strip enabled
- Sub-emitter chains: no chain deeper than 3 sub-emitters on dense burst systems
UI
- All non-interactive Image/RawImage components have Raycast Target disabled
- Static and dynamic elements on separate Canvases
- No Layout Groups on elements that update more than once per second
- All text uses TextMeshPro (zero legacy
Textcomponents) - Sprite Atlases created for icon sets and button states
- World Space Canvases justified (not used for screen UI)
-
CanvasGroup.blocksRaycasts = falseset alongsidealpha = 0for hidden panels - UI Toolkit: USS transitions use
translate/scale/rotateonly; nowidth/heightanimation
Streaming and Memory
- All audio files > 10 s use Streaming load type; all audio files < 1 s use Decompress on Load
- All textures checked for Read/Write Enabled trap
- All textures using correct BC compression format (BC5 for normals, BC1/BC3 for albedo, BC4 for single-channel masks)
- Texture max sizes appropriate to their use (distant props: 512 max; hero materials: 1024–2048 max)
- Mipmap Streaming enabled in Quality Settings for open-world or large scenes
- Addressables Analyze: Check Duplicate Bundle Dependencies ran and resolved
- All
Addressables.ReleaseAsset/ReleaseInstancecalls verified against all load paths - Memory Profiler baseline snapshot taken; compared against 20-minute play session for leaks
- No
RenderTexturecreated per-frame without explicitRelease()on the same frame
Physics Sweep
- Layer Collision Matrix audited — irrelevant layer pairs disabled
-
Physics.autoSyncTransformsset to false in physics-heavy scenes -
Time.fixedDeltaTimeset to minimum required for gameplay fidelity (0.02–0.033 s) - All
Physics.Raycasthot-path calls replaced withRaycastNonAlloc - Non-convex Mesh Colliders replaced with compound primitive colliders
-
Rigidbody.sleepThresholdincreased on static props and furniture -
Rigidbody.interpolationset toInterpolateon player character and camera targets - Physics Profiler module checked:
Physics.Processingwithin budget
Networking Sweep
- Tick rate matched to game type and client update rate
-
NetworkVariablemutations are change-driven, not per-frame - Object visibility culling configured (
CheckObjectVisibility) - Bandwidth measured and within target for minimum spec connection
- No
ServerRpccalls per frame (batched into per-tick structs) - Position quantization enabled on
NetworkTransformfor bandwidth reduction
Build and Cook Validation
- IL2CPP backend, .NET Standard 2.1, "Faster runtime" code gen selected
- Managed stripping at Medium or High;
link.xmlprotecting all reflection-dependent code - Build Report Inspector opened; largest assets identified and justified against budget
- Any asset > 50 MB in the build has a written rationale (streaming audio, large lightmap, etc.)
- Shader variant count logged (
log shader compilationin Player Settings); variants > 2,000 investigated -
ShaderVariantCollection.WarmUp()called during loading screen for all commonly used materials - Development Build disabled for Gold Master candidate build
- Final profile captured on minimum-spec hardware (not dev machine)
-
Application.BuildGUIDvisible in game's credits/version overlay for QA identification - All
asmdeffiles in place; compile time verified against baseline - Test Runner Performance Testing Extension baseline tests passing with no regression flags
- Build size compared against previous milestone; any increase > 10% investigated
Top 30 Most Common Mistakes
| # | Mistake | Why it hurts | Fix | Era |
|---|---|---|---|---|
| 1 | Camera.main in Update() | Native boundary crossing per frame | Cache in Start() | All |
| 2 | GetComponent<T>() in Update() without caching | Scene traversal per frame | Cache in Awake() | All |
| 3 | Instantiate without pooling | GC pressure + CPU spike on spawn | UnityEngine.Pool.ObjectPool<T> | All |
| 4 | String concatenation in Update() | Allocates new string object per frame | StringBuilder or update-on-change only | All |
| 5 | LINQ in hot paths | Enumerator + intermediate collection allocations | Manual for loops | All |
| 6 | Vector3 == for distance check (wrong semantics too) | Epsilon exact equality, not range check; sqrt cost | (a - b).sqrMagnitude < threshold * threshold | All |
| 7 | new WaitForSeconds() inside coroutine loop | Allocates heap object per iteration | private static readonly WaitForSeconds Wait1s = new(1f) | Pre-2020 |
| 8 | Find / FindObjectOfType / FindGameObjectsWithTag in tick | O(n) scene search per frame | Cache reference in Awake() or use event-based injection | All |
| 9 | Animator.SetFloat("Name", val) with strings in Update() | Internal string dictionary lookup per frame | Animator.StringToHash at init; pass int overloads | All |
| 10 | Layout Group on a UI element that updates every frame | GetComponent walk up transform hierarchy on dirty | Bake positions into anchors; avoid Layout Groups on dynamic elements | All |
| 11 | All Image components have Raycast Target = true by default | Graphic Raycaster iterates every enabled target on input | Disable Raycast Target on all non-interactive elements | All |
| 12 | SetActive(true/false) on a Canvas with many children | Full Canvas rebuild on activate/deactivate | Use Canvas.enabled or restructure | All |
| 13 | Read/Write Enabled on imported textures/meshes | Doubles memory (GPU copy + CPU copy) | Disable unless runtime pixel/vertex access required | All |
| 14 | Audio Clip "Decompress on Load" for long music files | Full decompressed PCM in RAM on load | Use Streaming load type for all music | All |
| 15 | Animator.cullingMode default AlwaysAnimate for off-screen NPCs | Full state machine evaluation regardless of visibility | Set CullCompletely for background characters | All |
| 16 | Mixed static and animating UI on one Canvas | Any change forces full Canvas rebuild | Split into static Canvas + dynamic Canvas | All |
| 17 | MaterialPropertyBlock breaking SRP Batcher | MPB opts renderer out of SRP Batcher, falls back to instancing or unoptimized draw calls | Use unique materials per variant; reserve MPB for explicit instancing paths | 2020-2022 LTS |
| 18 | Shader keyword variant explosion | 1,024 variants from 10 features; build time + memory | shader_feature_local for bake-time features; IPreprocessShaders for build-time stripping | All |
| 19 | Global keywords instead of local (_local) keywords | Global keyword space limited (~196 available); overflow causes silent fallback | Always use shader_feature_local / multi_compile_local | 2020-2022 LTS |
| 20 | Single Addressable bundle without dependency analysis | Shared assets duplicated across bundles → memory bloat | Run Analyze → Check Duplicate Bundle Dependencies | 2020-2022 LTS |
| 21 | Domain Reload enabled | 5–30 s iteration tax per Play press on large projects | Project Settings → Editor → Disable Domain Reload | 2019.3+ |
| 22 | Mono backend for shipping builds | JIT overhead, slower startup, weaker code stripping | Switch to IL2CPP for all shipping targets | All |
| 23 | LINQ GroupBy/OrderBy on dictionaries every frame | Multiple allocations + O(n log n) + enumerator boxing | Cache results; compute on change events, not per-frame | All |
| 24 | Coroutines never stopped on object destruction | Coroutine continues running on a destroyed object; NullReferenceException or worse | Stop coroutines in OnDestroy or use destroyCancellationToken (Unity 2022.2+) | All |
| 25 | Animator.Rebind() called in the update loop | Full Animator state rebuild; extremely expensive | Call only on rig change; never per-frame | All |
| 26 | GameObject.SetActive flicker-frame thrash | SetActive(false/true) in the same frame or alternate frames causes repeated full initialization | Restructure; use a state machine, not SetActive per-update | All |
| 27 | Shader Graph multi-compile keyword spam | Custom Function Nodes with conditional paths double variants if not scoped | Scope to shader_feature_local; wrap Custom Function Nodes in Sub Graphs with explicit keyword gating | 2020-2022 LTS |
| 28 | SRP Batcher disabled by Custom Function Node with non-deterministic output | SRP Batcher requires deterministic shader output to use cached constant buffers | Keep Custom Function Nodes deterministic; avoid state reads with non-constant results | 2020-2022 LTS |
| 29 | GPU Instancing not enabled on materials used with DrawMesh* APIs | DrawMeshInstanced / DrawMeshInstancedIndirect require #pragma instancing_options AND the material checkbox | Enable GPU Instancing on material + add instancing pragma | All |
| 30 | VFX Graph bounds set to Automatic | Disables culling entirely; every instance simulates every frame | Set Manual or Recorded bounds before shipping | All |
Knobs and Settings Cheat Sheet
Player Settings
| Setting | Options | Recommendation |
|---|---|---|
| Scripting Backend | Mono, IL2CPP | IL2CPP for shipping; Mono for development |
| IL2CPP Code Generation | Faster builds / Faster runtime | "Faster runtime" for shipping |
| API Compatibility Level | .NET Standard 2.1 / .NET Framework | .NET Standard 2.1 (smaller BCL, better stripping) |
| Managed Stripping Level | Disabled / Low / Medium / High | Medium (test) or High (with link.xml) |
| Graphics Jobs (Experimental) | Off / On | Off by default; test per-project |
| Multithreaded Rendering | On (default) | Keep On; disable only to see full render queue in Profiler |
| GPU Skinning | On / Off | On always where supported |
Quality Settings
| Setting | Low | Medium | High | Ultra |
|---|---|---|---|---|
| VSync | Off | Off | On (optional) | On |
| Anti-Aliasing | Off/FXAA | 2× MSAA or TAA | TAA or STP | STP or DLSS/FSR |
| Shadow Quality | Soft 2×2 PCF | Soft 4×4 PCF | Soft PCF | PCF or PCSS |
| Shadow Distance | 60 m | 100 m | 150 m | 200 m+ |
| Cascade Count | 1 | 2 | 3 | 4 |
| Texture Quality | Half | 3/4 | Full | Full |
| LOD Bias | 0.5 | 0.7 | 1.0 | 1.5 |
| Pixel Light Count | 1 | 2 | 4 | Unlimited |
URP Asset Key Knobs
| Setting | Recommendation |
|---|---|
| HDR | On (if Bloom used); Off otherwise |
| Render Scale | 1.0 baseline; 0.7–0.8 with STP (Unity 6+) |
| Anti-Aliasing (MSAA) | Off (use TAA/STP on High+); 2× on Medium |
| Main Light Shadow Resolution | 2048 for High; 1024 for Medium |
| Cascade Count | See Quality Settings table |
| Soft Shadows | On for High+; Off for Low |
| Additional Light Shadow Atlas | 2048 for High; disable on Low |
| GPU Resident Drawer | Instanced Drawing (Unity 6+, Forward+) |
| STP Upscaling | On at render scale < 1.0 (Unity 6+) |
| Forward+ | Enable when using GPU Resident Drawer |
HDRP Frame Settings
| Setting | Performance toggle |
|---|---|
| Ray Tracing | Off for low/medium tiers |
| Volumetric Fog | Depth Extent, Balance slider, Directional Only |
| Decal Count | Max Decal on Screen cap |
| SSGI / SSAO | Disable for low tier |
| Motion Blur | Disable for low/medium |
| Screen Space Reflections | Toggle; fall back to Reflection Probes |
| Bloom | On for high+; Off for low |
| Depth of Field | Off for low; Physical DOF only on Ultra |
Build Profile Toggles (Unity 2023.1+)
| Profile | Scripting Backend | Stripping | Dev Build | Autoconnect |
|---|---|---|---|---|
| Dev-Debug | Mono | Disabled | On | On |
| QA-Release | IL2CPP | Medium | On | Off |
| Gold-Master | IL2CPP | High | Off | Off |
| CI-Automated | IL2CPP | Medium | On | Off |
Quality Scalability Recommendations
[Low — Target: 720p/30 FPS, 5-year-old integrated graphics]
QualitySettings.shadows = ShadowQuality.HardOnly
QualitySettings.shadowDistance = 60
QualitySettings.shadowCascades = 1
QualitySettings.globalTextureMipmapLimit = 1
QualitySettings.antiAliasing = 0
QualitySettings.lodBias = 0.5
URP Render Scale = 0.7 (no STP)
HDR = Off
[Medium — Target: 1080p/60 FPS, GTX 1060 class]
QualitySettings.shadows = ShadowQuality.All (PCF 2x2)
QualitySettings.shadowDistance = 100
QualitySettings.shadowCascades = 2
QualitySettings.antiAliasing = 2
QualitySettings.lodBias = 0.7
URP Render Scale = 0.85
HDR = On
[High — Target: 1440p/60 FPS, RTX 2080 class]
QualitySettings.shadowDistance = 150
QualitySettings.shadowCascades = 3
QualitySettings.antiAliasing = 0 (TAA/STP)
QualitySettings.lodBias = 1.0
URP Render Scale = 1.0 (or 0.85 with STP)
HDR = On
SSAO = Medium
[Ultra — Target: 4K/60 FPS or 1440p/120 FPS]
QualitySettings.shadowDistance = 200
QualitySettings.shadowCascades = 4
URP Render Scale = 1.0 + DLSS/FSR/STP Quality mode
HDR = On
All effects On
Physics Project Settings Reference
| Setting | Location | Low tier | High tier |
|---|---|---|---|
fixedDeltaTime | Project Settings → Time | 0.033 s (30 Hz) | 0.02 s (50 Hz) |
maximumAllowedTimestep | Project Settings → Time | 0.033 s | 0.1 s (default) |
sleepThreshold (global) | Project Settings → Physics | 0.1 | 0.005 (default) |
defaultContactOffset | Project Settings → Physics | 0.02 | 0.01 (default) |
autoSyncTransforms | Project Settings → Physics | false | false |
| Solver Type | Project Settings → Physics | Projected Gauss-Seidel | Temporal Gauss-Seidel (Unity 2022+) |
| Broadphase Type | Project Settings → Physics | SAP (Sweep and Prune) | MBP (Multi-Box Pruning, large open worlds) |
Solver Type note: Temporal Gauss-Seidel (TGS, Unity 2022+) provides more stable stacking and joint constraints at the same iteration count as Projected Gauss-Seidel (PGS). TGS is more expensive per-iteration but often requires fewer iterations for equivalent quality, resulting in a net neutral or slight win for complex stacking scenarios.
Broadphase Type note: Multi-Box Pruning (MBP) is preferred for large open worlds where objects span a wide spatial distribution. Sweep and Prune (SAP) is faster for scenes where objects cluster in a small area.
Audio Project Settings Reference
| Setting | Location | Recommendation |
|---|---|---|
| DSP Buffer Size | Project Settings → Audio | Best Latency for input-sensitive games; Default for most titles |
| Max Virtual Voices | Project Settings → Audio | 512 (default); reduce to 256 if audio CPU cost is high |
| Real Voices | Project Settings → Audio | 32 (default); increase only if voice-stealing is audible |
| Spatializer Plugin | Project Settings → Audio | Built-in (default); Steam Audio for physics-based occlusion |
| Sample Rate | Project Settings → Audio | 44100 Hz default; 22050 Hz acceptable for most game SFX |
Editor Performance Settings Reference (All versions)
These settings have zero shipping build impact but dramatically affect daily iteration speed:
| Setting | Location | Recommendation |
|---|---|---|
| Disable Domain Reload | Project Settings → Editor → Enter Play Mode | Enable immediately on all projects |
| Disable Scene Reload | Project Settings → Editor → Enter Play Mode | Enable; requires stateless startup code |
| Import Worker Count | Preferences → General | Leave at default (auto); increase on machines with >16 cores |
| Script Changes While Playing | Preferences → External Tools | Recompile and Continue Playing (not Stop Playing and Recompile) |
| Preferred Version Control | Project Settings → Version Control | Visible Meta Files (never Hidden Meta Files) |
Editor Iteration Performance (All versions)
Condensed from Sebastian Schöner — Why Your Unity Project Is Slow (suggested by Michał Szymerski). Runtime performance and editor iteration performance are separate problems — a project can ship at 60 FPS and still waste hours of developer time daily on slow domain reloads, imports, and Play Mode entry.
Domain reloads — every script change invalidates all C# state: serialize, unload domain, boot new domain, deserialize, re-initialize (~10 s typical, worse on large projects):
- Minimize work in static constructors and
[InitializeOnLoad]— they run on every domain reload. - Cache expensive lookups (e.g. asset searches) in
SessionStateinstead of recomputing per reload. - 30–50% of domain reload time can go into just JIT-compiling code — less code loaded means faster reloads; audit and remove unused packages and Asset Store items.
Serialization overhead:
- Avoid serializing large data structures (big arrays, long strings); mark fields you don't need with
[NonSerialized]. - Close unused editor windows — they are ScriptableObjects that get serialized on every reload.
- Minimize custom
ISerializationCallbackReceiverimplementations.
Script compilation:
- Balance asmdef granularity — too many assemblies adds per-assembly overhead, too few loses parallelization.
- Profile Roslyn analyzers and source generators; consider disabling Burst compilation during iteration.
- Disable Auto Refresh (Preferences) and refresh manually to batch multiple script changes into one import + compile + reload cycle.
Asset importing:
- Use Presets for default import settings; disable Generate Physics Shape on sprites that don't need it.
- Avoid custom
AssetPostprocessors where possible;OnPostprocessAllAssetsreceives every imported asset — profile it regularly. - Keep source assets (PSD, blend files) out of the Assets folder; if unavoidable, place them in hidden folders (prefixed with
.or suffixed with~). - Enable parallel import (Unity 2022+).
Asset loading / editor scripting:
- Never call
AssetDatabase.FindAssets()in initialization code — cache paths inSessionState. - Use
TypeCacheinstead of reflection for type scans. - Batch multiple
AssetDatabaseoperations withStartAssetEditing/StopAssetEditing.
Entering Play Mode:
- Enable domain reload + scene reload skipping (see table above).
- Measure
PostProcessScene,RuntimeInitializeOnLoad, andplayModeStateChangedcallbacks — Asset Store packages sometimes burn seconds there. - Move expensive precomputation, pooling, shader warmup, and data loading to lazy initialization instead of
Awake()/Start(). - For large static geometry scenes, consider
BatchRendererGroupinstead of static batching — static batching is recomputed on Play Mode entry.
Prefab editing:
- Edit in Prefab Mode to batch all changes before saving — pay the import cost once, not per edit.
- Avoid deeply nested prefab hierarchies; changing a source prefab re-imports it and all dependents.
- Close large scenes before editing widely-used source prefabs to avoid updating thousands of instances.
Unity ↔ UE Concept Bridge
This guide's companion document is the UE4/UE5 Optimization Guide. If you use both engines or are migrating, these equivalences prevent re-learning the same concepts under different names.
| Unity Concept | UE Equivalent | Notes |
|---|---|---|
| SRP Batcher | PSO (Pipeline State Object) caching | Both reduce per-draw-call GPU state setup overhead. SRP Batcher is more automatic; PSO requires explicit pipeline compilation. |
| GPU Resident Drawer (Unity 6+) | Nanite clustering / HLOD | Both reduce draw call count via GPU-driven instance batching. UE5 Nanite is more ambitious (full virtualized geometry); Unity GRD is an instancing layer. |
| Render Graph (URP Unity 6+) | RDG (Render Dependency Graph, UE4+) | Both use declarative pass registration with automatic resource lifetime and pass culling. Unity's is newer and simpler; UE's RDG is battle-tested across all console generations. |
| Shader Graph | Material Editor (UE) | Both are visual, node-based shader authoring tools. Unity Shader Graph generates HLSL; UE Material Editor generates HLSL/GLSL per platform. |
shader_feature_local / multi_compile_local | Material Static Switch Parameters | Both select shader paths at compile time without runtime branching overhead. |
| Addressables | UE Asset Manager + Primary Asset Labels | Both provide reference-counted async asset loading over a bundle/pak layer. Addressables is simpler to configure; UE Asset Manager is more deeply integrated with streaming. |
| Entity Component System (ECS) | Mass AI / Data-Oriented Entities | Both are cache-friendly entity-component architectures for high entity counts. Unity's is more general-purpose; UE's Mass system is specifically targeted at AI crowd simulations. |
| Burst Compiler | ISPC / Multithreaded execution in UE | Both generate SIMD-optimized code for data-parallel workloads. Burst is more accessible (C# + [BurstCompile]); ISPC requires a separate language. |
| Light Probes + APV | Distance Field Ambient Occlusion + Lumen (indirect) | Unity APV ≈ UE5 Lumen's indirect lighting cache conceptually. Both provide per-pixel GI for dynamic objects. Lumen requires no baking but is GPU-heavier; APV is baked but near-zero runtime cost. |
| Progressive Lightmapper | CPU Lightmass (UE4) / GPU Lightmass (UE4.26+) | Both are path-tracing bakers. UE GPU Lightmass is broadly comparable to Unity's Progressive GPU Lightmapper in workflow. |
| STP (Spatial-Temporal Post-Processing) | TSR (Temporal Super Resolution) | Both are software temporal upsamplers built into the engine, requiring no vendor SDK. TSR (UE5) is more mature with wider platform support; STP (Unity 6+) is newer but compute-only. |
| DLSS / FSR / XeSS plugins | DLSS / FSR / XeSS plugins | Identical vendor technology; both engines require separate plugin/SDK installation. UE has more polished DLSS integration via the NVIDIA DLSS for Unreal plugin. |
| LOD Group + LOD Bias | LOD system + r.StaticMeshLODDistanceScale | Conceptually identical. Unity uses screen-space coverage percentages; UE uses distance-based LOD transitions scaled by a console variable. |
| Occlusion Culling (baked) | Precomputed Visibility (UE4) | Both precompute per-cell visibility at edit time for closed environments. UE5 with Nanite largely eliminates the need for PVS; Unity has added GPU occlusion culling (Unity 6+) as an alternative. |
| VFX Graph (GPU particles) | Niagara GPU simulation | Both are compute-shader particle simulators with a visual graph authoring interface. Niagara has broader platform support and more advanced simulation modules; VFX Graph requires compute shader capable hardware. |
| Netcode for GameObjects | Native replication (AGameMode/APlayerController) | Both are server-authoritative networked replication systems. UE's native replication is more mature and deeply integrated with the engine's actor lifecycle. |
NetworkVariable<T> | Replicated UPROPERTY | Both synchronize state from server to clients. UE's replicated properties have more granular RepNotify controls; NGO's NetworkVariable is simpler but less flexible. |
| Unity Physics (DOTS) | Chaos Physics (UE5) | Both are data-oriented physics systems written to run in parallel jobs. Chaos is more stable in production as of 2024; Unity Physics is simpler but less feature-complete. |
Version Migration Notes
For teams moving between major Unity versions, these are the performance-impacting breaking changes that most commonly cause regressions.
Migrating from Built-in RP to URP (Pre-2020) to (2020-2022 LTS)
This is the highest-impact migration in Unity's history for most indie teams.
- Surface Shaders do not exist in URP. Rewrite all Surface Shaders as Unlit or Lit ShaderLab programs using URP's ShaderLibrary includes, or recreate in Shader Graph. This is the largest migration cost.
- Post-Processing Stack v2 is replaced by URP's Volume system. Remove
com.unity.postprocessing; recreate effects as Volume Overrides in the Universal Renderer. - All custom
CommandBufferinjections must be rewritten asScriptableRendererFeature+ScriptableRenderPass(pre-Unity 6) or Render Graph (Unity 6+). - SRP Batcher is automatically active in URP. Ensure all shaders have
CBUFFER_START(UnityPerMaterial)blocks or batching will silently fall back. - Dynamic Batching: Recommended to disable in URP. SRP Batcher handles the same use cases more efficiently. Unity draw call optimization
- GPU Instancing vs SRP Batcher: You cannot use both simultaneously on the same renderer. Decide per use case: SRP Batcher for heterogeneous static scenes; GPU Instancing for thousands of identical dynamic objects.
- Reference: Migrating from Built-in RP to URP
Migrating from Unity 2020/2021 to Unity 2022 LTS (2020-2022 LTS era internal)
- DOTS Entities 1.0 was released for Unity 2022.2. The entire ECS API changed:
Entities.ForEachdeprecated,ISystemintroduced, baking system replaced conversion workflow. ECS projects from Unity 2020/2021 require significant migration. DOTS migration guide - UI Toolkit became production-ready for runtime UIs in Unity 2022 LTS. The IMGUI backend no longer receives feature investment for runtime UI. Toolkit-based UIs in 2020/2021 may need API updates.
- GraphicsFormat API changed for some render texture formats between 2021 and 2022. RenderTexture creation using deprecated format enums will produce console warnings.
- Hybrid Renderer V2 renamed to Entities Graphics (
com.unity.entities.graphics). The package name changed; updatemanifest.json.
Migrating to Unity 6 (2020-2022 LTS) to (Unity 6+)
- Render Graph is mandatory in URP. All
ScriptableRenderPass.Execute()overrides must be migrated toRecordRenderGraph(). Unity 6.0 ships a compatibility mode (Compatibility Modetoggle in the Universal Renderer asset) that keeps old code working, but it is removed in a later release. Migrate immediately. Render Graph introduction - GPU Resident Drawer is opt-in in URP, on-by-default in HDRP. If Static Batching was your primary batching strategy, disable it and switch to GRD for GPU-side instancing.
- Adaptive Probe Volumes (APV) become the recommended default for new projects. Existing projects using Light Probe Groups still work but miss APV features (per-pixel sampling, sky occlusion, scenario blending).
- Built-in RP is deprecated in Unity 6.5. Not removed yet, but no new features. If your project uses Built-in RP, begin URP migration planning. BIRP deprecation notice
- Mesh LOD automatic generation added in Unity 6.2 — re-import meshes to take advantage of shared-vertex-buffer LODs.
Awaitable(introduced Unity 2023.1, part of the Unity 6 lifecycle) replaces coroutine-heavy async patterns. Gradually migrate long-running coroutines toAwaitablewhere value return or cancellation is needed.destroyCancellationTokenavailable on allMonoBehaviourfrom Unity 2022.2 — use it to automatically cancel async operations when the object is destroyed.
Performance Regressions to Watch After Unity Version Upgrades
Every major version upgrade should be followed by a Profile Analyzer comparison against the pre-upgrade baseline. Common regression patterns:
| Symptom | Likely cause | Investigation step |
|---|---|---|
| Higher draw call count after upgrade | SRP Batcher compatibility broken by new Unity shader changes | Frame Debugger → SRP Batcher batch breakdown |
| GC spikes introduced after upgrade | Unity internal API change allocating where it didn't before | CPU Hierarchy → sort by GC Alloc; look for new Unity engine entries |
| Shader compilation stutter at first use | New shader variants generated by updated URP/HDRP | Re-run ShaderVariantCollection.WarmUp() with updated collection |
| Physics FixedUpdate over budget | Changed default physics settings between versions | Compare Project Settings → Physics between version snapshots |
| Memory increase | Texture format defaults changed, or new Unity assets auto-imported | Memory Profiler snapshot diff; check Textures category |
Bibliography and Further Reading
Official Documentation
- Unity Profiler (Manual)
- Unity profiling best practices
- Unity profiling applications
- SRP Batcher (Manual)
- GPU instancing (Manual)
- GPU Resident Drawer (URP)
- Render Graph introduction (URP)
- Write a render pass using Render Graph
- Rendering paths comparison (URP)
- Adaptive Probe Volumes concept (Unity 6)
- APV usage guide (Unity 6)
- STP upscaler (Unity 6)
- Shadow cascades (Unity 6)
- Mixed lighting (Unity 2019)
- Occlusion Culling (Unity 6)
- GPU Occlusion Culling URP
- LOD Group (Manual)
- BatchRendererGroup API
- Entities Graphics (Manual)
- Burst Compiler (Manual)
- Job System (Manual)
- Garbage collection best practices
- Incremental GC (Manual)
- IL2CPP scripting backend
- Shader variants (Manual)
- Shader variant stripping
- Custom Function Node (Shader Graph)
- Draw call batching (Manual)
- Texture compression formats (Manual)
- Mipmap Streaming (Manual)
- Mesh.indexFormat (Scripting API)
- Addressables (Manual)
- Per-frame event optimization (Unity 6)
- Physics autoSyncTransforms
- AudioClip import settings
- Animation performance (Manual)
- UI Optimization Tips (Unity)
- UI system comparison (Unity 6)
- Netcode for GameObjects — Tick and Update Rates
- Entities 1.0 — Baker Overview
- Entities 1.0 — SystemAPI.Query
- Profile Analyzer
- Memory Profiler
- ProfilerMarker API
- Standalone Profiler
- Choose a render pipeline
- BIRP deprecated in Unity 6.5
- Choosing your particle system solution
- VFX Graph Visual Effect Bounds
- URP Rendering Layers
- Camera Stacking (URP)
- HDRP Volumetric Fog
- Optimizing UI Toolkit
Unity Blog
- Saving memory with Addressables
- Unite 2022 – Performance in Unity
- Unity SRP Batcher — official deep dive
Community Guides and Blogs
- TheGameDev.Guru — Draw Call Batching: The Ultimate Guide
- TheGameDev.Guru — Physics autoSyncTransforms
- TheGameDev.Guru — Render Modes and Graphics Jobs
- Tarodev — Unity Optimization Tips — YouTube channel with practical optimization walkthroughs
- Code Monkey — Unity Tutorial Channel — DOTS, ECS, UI, practical Unity systems
- Catlike Coding — Unity Tutorials — Deep-dive rendering, lighting, and shader tutorials
- Acerola — Unity Rendering — Shader math and custom rendering
- Game Dev Guide — Unity — UI, animation, editor tooling
- NotSlot — Unity Optimization — Performance-focused Unity content
- fazz.dev — SRP Batcher deep dive
- Azur Games — SRP Batcher optimization
- Angry Shark Studio — UI Toolkit vs UGUI 2025
- Coffee Brain Games — Reducing Compile Time with asmdef
- Sebastian Schöner — Why Your Unity Project Is Slow — Editor iteration performance: domain reloads, compilation, imports
- Generalist Programmer — Burst Compiler Guide
- Embrace.io — GC spikes in Unity
- JetBrains Rider — Camera.main inspection
- Nathan Reed — Understanding BCn texture compression
- Game Developer — Unity Audio Import Optimisation
- Steam Audio Unity Guide
- Mirror — Lag Compensation
- HackerNoon — Unity 2023.1 Awaitable Class
- NativeContainer allocators guide — Outscal
- Techarthub — Texture Compression in Unity
- Chunk iteration in Entities 1.0 — Coffee Brain Games
YouTube Channels and Videos
- Tarodev — Practical C#, DOTS, performance
- Code Monkey — DOTS, ECS, UI, Unity systems
- Catlike Coding — Rendering, shaders, mathematics
- Sebastian Lague — Procedural generation, creative coding
- Acerola — Custom rendering, shader mathematics
- Game Dev Guide — Editor tooling, animation, UI
- NotSlot — Performance, ECS, systems architecture
- Brackeys (legacy) — Legacy tutorials; many patterns no longer optimal in Unity 6 but useful for concept background
- DOUBLE Unity Animation Performance — GPU Skinning
- How to use Adaptive Probe Volumes Unity 6
- Unity 6 Mesh LOD Tutorial
- DOTS Animation — Code Monkey
- Netcode for GameObjects Tutorial
Books
- "Unity Game Optimization, 3rd Edition" — Chris Dickinson. The most comprehensive single-volume reference for Unity performance. Covers CPU, GPU, memory, physics, and audio in depth. Published by Packt; available in print and digital.
- "Game Programming Patterns" — Robert Nystrom (free at gameprogrammingpatterns.com). Not Unity-specific but covers object pooling, event queues, component patterns, and data locality — directly applicable.
- "Data-Oriented Design" — Richard Fabian (free at dataorienteddesign.com). The theoretical foundation for DOTS/ECS; useful for understanding why ECS outperforms OOP at scale.
Repositories and Samples
- Unity DOTS Samples — GitHub — Official ECS, Jobs, and Burst samples
- Unity Boss Room — GitHub — Production NGO multiplayer reference
- Unity HLOD System — GitHub — Hierarchical LOD for large scenes
- Compilation Visualizer — GitHub (Needle Tools) — asmdef dependency audit
- Awesome Unity — GitHub — Curated Unity resources list
- XeSS Unity Plugin — GitHub (Intel) — Open-source XeSS integration
- UniTask — GitHub (Cysharp) — Zero-allocation async/await for Unity
Community
- r/Unity3D — Broad community; performance discussions, gotcha reports, version-specific discoveries
- Unity Forums — Official support forums; engine team responses on ambiguous behavior
- Unity Discord (official) — Real-time community help
- Brackeys Discord (community) — Large community, accessible for beginners
- Unity Road Map — Track upcoming features before they ship to avoid investing in soon-to-be-superseded patterns
This guide covers Unity 2018/2019 (Pre-2020 era), Unity 2020–2022 LTS, and Unity 6 (October 2024 release, 6000.x). All API references verified against official Unity documentation at time of writing. Era tags indicate the first version where a feature became stable and production-recommended, not necessarily where it was first introduced in preview.