Skip to content

Performance Optimization

CPU Optimization

Cache Component References

Never call GetComponent in Update loops. Cache in Awake:

// GOOD
private Rigidbody _rb;
private void Awake() => _rb = GetComponent<Rigidbody>();

// BAD - allocates and searches every frame
private void Update()
{
    GetComponent<Rigidbody>().AddForce(Vector3.up);
}

Avoid Find Methods at Runtime

FindFirstObjectByType, FindObjectsByType, and GameObject.Find are expensive. Use them only during initialization, never in Update.

Prefer: - Direct serialized references - Events and callbacks - Runtime sets (ScriptableObject pattern) - Dependency injection

String Operations

// BAD - allocates garbage every frame
_text.text = "Score: " + score.ToString();

// GOOD - zero allocation with TMP
_tmpText.SetText("Score: {0}", score);

// GOOD - StringBuilder for complex strings
private readonly StringBuilder _sb = new(64);
private void UpdateUI()
{
    _sb.Clear();
    _sb.Append("HP: ").Append(_health).Append('/').Append(_maxHealth);
    _text.text = _sb.ToString();
}

Avoid Allocations in Hot Paths

These allocate on the heap and trigger GC:

Allocation Source Alternative
new List<T>() in Update Pre-allocate, reuse
LINQ (Where, Select) Manual for/foreach loops
string + string StringBuilder or TMP.SetText
foreach on non-generic IEnumerable Use for with index
params arrays Use explicit overloads
Boxing (int to object) Use generics
Closures / lambdas capturing locals Cache delegates

Physics - NonAlloc Variants

// BAD - allocates array every call
RaycastHit[] hits = Physics.RaycastAll(origin, direction, maxDist);

// GOOD - reuse pre-allocated buffer
private readonly RaycastHit[] _hitBuffer = new RaycastHit[16];
int count = Physics.RaycastNonAlloc(origin, direction, _hitBuffer, maxDist);

Also: OverlapSphereNonAlloc, OverlapBoxNonAlloc, SphereCastNonAlloc, BoxCastNonAlloc.

Object Pooling

Replace Instantiate/Destroy patterns with object pools:

// Use Unity's built-in ObjectPool<T> (Unity 2021+)
private ObjectPool<GameObject> _pool;

private void Awake()
{
    _pool = new ObjectPool<GameObject>(
        createFunc: () => Instantiate(_prefab),
        actionOnGet: obj => obj.SetActive(true),
        actionOnRelease: obj => obj.SetActive(false),
        actionOnDestroy: obj => Destroy(obj),
        maxSize: 50
    );
}

Cache Camera.main

Camera.main calls FindGameObjectWithTag("MainCamera") internally:

// BAD
void Update() { Camera.main.ScreenToWorldPoint(...); }

// GOOD
private Camera _mainCam;
void Awake() => _mainCam = Camera.main;

Burst Compiler and Jobs

For CPU-intensive work, use the Jobs system with Burst:

[BurstCompile]
public struct CalculateDistancesJob : IJobParallelFor
{
    [ReadOnly] public NativeArray<float3> Positions;
    public NativeArray<float> Distances;
    public float3 Target;

    public void Execute(int index)
    {
        Distances[index] = math.distance(Positions[index], Target);
    }
}

GPU Optimization

Draw Call Batching

  • Static Batching: Mark non-moving objects as Static. Unity combines their meshes at build time.
  • Dynamic Batching: Automatic for small meshes (< 300 vertices). Limited usefulness.
  • GPU Instancing: Enable on materials for many identical objects (trees, grass, debris).
  • SRP Batcher: Enabled by default in URP. Requires shaders with CBUFFER-wrapped properties.

Render Graph (Unity 6)

The Render Graph backend in URP/HDRP optimizes rendering passes automatically: - Eliminates redundant render target switches - Reduces memory bandwidth - Use Render Graph custom passes instead of extra cameras

Occlusion Culling

Bake occlusion data so Unity skips rendering objects hidden behind walls. Set up in Window > Rendering > Occlusion Culling.

LOD Groups

Reduce polygon count for distant objects: - LOD0: Full detail (close) - LOD1: Medium detail (mid-range) - LOD2: Low detail (far) - Culled: Not rendered (very far)

Shader Complexity

  • Minimize shader variants with shader_feature_local
  • Use half precision on mobile
  • Avoid complex math in fragment shaders when possible
  • Use shader stripping to remove unused variants from builds

Memory Optimization

  • Compress textures per platform (ASTC for mobile, BC7 for desktop)
  • Use Sprite Atlases for 2D to reduce draw calls
  • Use Addressables for on-demand loading instead of preloading everything
  • Call Resources.UnloadUnusedAssets() after major scene transitions
  • Use Addressables.Release(handle) to free loaded assets

Profiling Tools

  • Profiler Window (Window > Analysis > Profiler): CPU, GPU, memory, rendering, audio, physics
  • Frame Debugger (Window > Analysis > Frame Debugger): Step through draw calls one at a time
  • Memory Profiler: Detailed memory snapshots and comparison
  • Profile Analyzer: Compare profiler captures for regression testing

Mobile-Specific

  • Target 30fps for battery-friendly games, 60fps for action games
  • Reduce fill rate: minimize overdraw, use simpler shaders
  • Compress textures to ASTC (Android/iOS)
  • Disable real-time shadows on low-end devices
  • Use Adaptive Performance providers for automatic quality scaling