Unity Mobile Optimization: The Complete Performance Guide

Master Unity mobile optimization — from draw call batching and texture compression to garbage collection and UI Canvas splitting. Practical tips that ship.

Your Unity game runs beautifully in the editor at 144 FPS on your desktop GPU. Then you build for Android, install it on a mid-range phone, and it chugs at 18 FPS while the device gets hot enough to fry an egg. Sound familiar?

TL;DR: Mobile performance requires targeting 70% of a device's peak GPU budget to survive thermal throttling. The biggest wins come from keeping draw calls under 200 (use static batching and GPU instancing), compressing textures with ASTC, eliminating per-frame garbage collection allocations, splitting UI into static and dynamic canvases, and profiling on the lowest-spec target device. This guide covers all of these with production-ready C# code and a pre-launch checklist.

Mobile optimization isn't a nice-to-have polish step — it's a fundamental requirement that should inform your architecture from day one. Mobile GPUs have a fraction of the power of desktop hardware, mobile CPUs throttle aggressively under heat, battery drain directly impacts player retention, and the device landscape spans everything from flagship to budget.

The #1 rule of mobile optimization: measure first, optimize second. Never guess at bottlenecks. The Unity Profiler exists for a reason.

Why Mobile Performance Is Different

Desktop GPUs are designed for sustained high performance with active cooling. Mobile GPUs are designed for power efficiency with passive cooling. This means thermal throttling is your constant enemy — a phone that starts at 60 FPS can drop to 30 FPS after two minutes of sustained load as the chip temperature rises and the OS reduces clock speeds.

The practical consequence: your game needs to run well below the device's peak capability. Target 70% of the available GPU budget so thermal throttling doesn't destroy the experience. If your game barely hits 60 FPS when the device is cold, it will drop to 30 FPS within minutes of gameplay.

Draw Calls and Batching

Draw calls are the #1 GPU bottleneck in most mobile games. Every time Unity tells the GPU to render a mesh with a specific material, that's a draw call. Mobile GPUs handle 100-200 draw calls comfortably; above 300, you're in trouble.

Static Batching

For objects that never move (environment, decorations, buildings), mark them as Static in the Inspector. Unity combines their meshes at build time into larger batches, dramatically reducing draw calls.

Dynamic Batching and GPU Instancing

For moving objects that share the same material, enable GPU Instancing on the material. This tells the GPU to render multiple instances of the same mesh in a single draw call. Configure it in your project settings and material inspector:

C#
using UnityEngine;
using UnityEngine.Rendering;

public class BatchingSetup : MonoBehaviour
{
    [SerializeField] private Material sharedMaterial;

    void Start()
    {
        // Enable GPU Instancing on the shared material at runtime
        sharedMaterial.enableInstancing = true;

        // For SRP Batcher compatibility, ensure shaders use
        // constant buffers (CBUFFER_START / CBUFFER_END)
    }

    // Use MaterialPropertyBlock to vary per-instance data
    // without breaking batching
    public void SetInstanceColor(Renderer rend, Color color)
    {
        MaterialPropertyBlock block = new MaterialPropertyBlock();
        rend.GetPropertyBlock(block);
        block.SetColor("_Color", color);
        rend.SetPropertyBlock(block);
    }
}

Texture Compression: ASTC vs ETC2

Uncompressed textures are the silent memory killer on mobile. A single 2048x2048 RGBA texture consumes 16MB of memory uncompressed. With ASTC compression, that same texture is 1-4MB depending on the block size.

  • ASTC: The modern standard. Supported on all devices from ~2015 onward. Variable block sizes (4x4 for high quality, 8x8 for small size). Use this as your default.
  • ETC2: Fallback for very old OpenGL ES 3.0 devices. Supported everywhere but slightly lower quality than ASTC at the same file size.
  • Rule of thumb: Use ASTC 6x6 for most game textures, ASTC 4x4 for UI and text that needs to stay sharp, and ASTC 8x8 for large backgrounds where compression artifacts are less visible.

Mesh Optimization: LODs and Polygon Budgets

Mobile polygon budgets are tighter than you think. A good target for the entire scene is 100K-200K triangles. A single character should be 2K-5K triangles (compare to 50K-100K on console). Use LOD Groups to automatically swap to lower-poly meshes at distance.

For games with many small objects (collectibles, obstacles, particles), consider combining meshes at runtime to reduce draw calls while keeping polygon counts manageable.

Physics Optimization

Unity's physics engine runs on a fixed timestep, and on mobile, every millisecond counts. The default fixed timestep of 0.02 (50 Hz) is often more precision than a mobile game needs.

C#
using UnityEngine;

public class PhysicsOptimizer : MonoBehaviour
{
    void Awake()
    {
        // Reduce physics update rate for mobile
        // 30 Hz is enough for most mobile games
        Time.fixedDeltaTime = 0.033f;

        // Increase sleep threshold so idle Rigidbodies
        // stop consuming physics resources sooner
        Physics.defaultSolverIterations = 4;    // Default is 6
        Physics.defaultSolverVelocityIterations = 1; // Default is 1
        Physics.sleepThreshold = 0.1f;          // Default is 0.005

        // Disable auto-sync transforms — manually sync
        // only when needed for physics queries
        Physics.autoSyncTransforms = false;
    }
}

// Also configure the Layer Collision Matrix in:
// Edit > Project Settings > Physics
// Disable collisions between layers that should never interact.
// Example: UI layer should never collide with anything.

The Layer Collision Matrix is one of the most overlooked optimizations. By default, every layer can collide with every other layer. If your game has 8 layers, that's 36 layer pairs the physics engine checks every frame. Disabling unnecessary pairs (e.g., Background vs UI, Enemies vs Enemies) can cut physics work in half.

Garbage Collection: The Frame Rate Killer

Garbage collection (GC) spikes are the most common cause of visible stuttering on mobile. Every time you allocate heap memory (with new, string concatenation, LINQ, or boxing), the garbage collector eventually has to clean it up — and when it does, your frame freezes for 5-20ms.

Bad vs Good Patterns

C#
using UnityEngine;
using System.Text;
using System.Collections.Generic;

public class GCPatterns : MonoBehaviour
{
    // ========== BAD: Allocates every frame ==========

    void Update_Bad()
    {
        // BAD: String concatenation creates new strings each frame
        string label = "Score: " + score.ToString();

        // BAD: GetComponent every frame allocates and is slow
        var rb = GetComponent<Rigidbody>();

        // BAD: LINQ allocates enumerator objects
        var enemies = FindObjectsOfType<Enemy>()
            .Where(e => e.IsAlive)
            .ToList();

        // BAD: new List every frame
        List<Vector3> path = new List<Vector3>();
    }

    // ========== GOOD: Zero per-frame allocations ==========

    private StringBuilder sb = new StringBuilder(64);
    private Rigidbody cachedRb;
    private List<Enemy> enemyCache = new List<Enemy>(32);
    private List<Vector3> pathCache = new List<Vector3>(64);
    private int score;

    void Start()
    {
        cachedRb = GetComponent<Rigidbody>();
    }

    void Update_Good()
    {
        // GOOD: StringBuilder reuse avoids string allocations
        sb.Clear();
        sb.Append("Score: ");
        sb.Append(score);
        string label = sb.ToString();

        // GOOD: Cached reference, zero allocation
        cachedRb.AddForce(Vector3.up);

        // GOOD: Reuse list, manual filtering
        enemyCache.Clear();
        // Use a pre-maintained list instead of FindObjectsOfType

        // GOOD: Reuse list
        pathCache.Clear();
    }
}

Object pooling is the ultimate GC prevention technique — instead of instantiating and destroying objects, you recycle them from a pool. Our Object Pool script handles this pattern with a clean API.

UI Canvas Optimization

Unity's UI Canvas system rebuilds the entire canvas mesh whenever any element on it changes. If your HUD score text updates every frame and it shares a Canvas with 50 static UI elements, all 50 elements get rebuilt every frame.

C#
using UnityEngine;
using UnityEngine.UI;

// Split your UI into separate canvases:
// - StaticCanvas: health bars, frames, backgrounds (rarely changes)
// - DynamicCanvas: score text, timers, damage numbers (changes often)
// - OverlayCanvas: popups, menus (only active when needed)

public class UIOptimizer : MonoBehaviour
{
    [Header("Disable Raycast Target on non-interactive elements")]
    [SerializeField] private Graphic[] nonInteractiveElements;

    void Start()
    {
        // Disable raycast on every element that doesn't need tap detection
        // This reduces the work the GraphicRaycaster does each frame
        foreach (var graphic in nonInteractiveElements)
        {
            graphic.raycastTarget = false;
        }
    }

    // Also consider:
    // 1. Use TextMeshPro instead of legacy Text (better batching)
    // 2. Avoid Layout Groups on dynamic content (expensive recalculation)
    // 3. Disable Canvas components on off-screen UI instead of
    //    setting alpha to 0 (alpha 0 still rebuilds)
    // 4. Pool damage number / floating text objects
}

Quick win: search your project for UI elements with Raycast Target enabled but no click handler. Every one you disable saves work on the GraphicRaycaster. On a complex HUD, this alone can save 1-2ms per frame.

Shader Tips for Mobile

Shaders are where mobile GPUs struggle most. Desktop GPUs have massive ALU counts and fast memory bandwidth; mobile GPUs have neither. Follow these rules:

  • Use URP mobile shaders: Universal Render Pipeline's mobile-optimized shaders are designed for tile-based GPUs. Don't use Standard/Legacy shaders.
  • Avoid alpha blending: Transparent objects break tile-based rendering and cause overdraw. Use alpha cutout (clip) instead where possible.
  • Minimize shader passes: Each pass is essentially another draw call. Single-pass shaders only.
  • Avoid complex math in fragment shaders: Move calculations to the vertex shader where possible. Per-vertex is computed once per vertex; per-pixel is computed once per pixel (and there are millions of pixels).
  • Bake lighting: Real-time lights are expensive. Use baked lightmaps for static geometry and limit real-time lights to 1-2 (directional + one point light).

Using the Unity Profiler

The Unity Profiler is your most important optimization tool. Connect it to a physical device (not the editor — editor profiling is misleading) and look for these common bottlenecks:

  • CPU > Scripts: Look for spikes. Click on spike frames to see which methods consumed the most time. Common culprits: FindObjectOfType, GetComponent in Update, LINQ in hot paths.
  • CPU > Physics: If physics takes more than 3-4ms per frame, reduce fixed timestep, simplify colliders (use boxes instead of mesh colliders), or reduce the collision matrix.
  • CPU > UI: Canvas rebuild times. If Canvas.BuildBatch or Canvas.SendWillRenderCanvases is high, you need canvas splitting.
  • GPU > Rendering: If GPU time is high but CPU is fine, you're fillrate-bound (too many pixels with complex shaders or overdraw) or geometry-bound (too many triangles).
  • Memory: Watch for GC.Alloc entries in the CPU profiler. Every allocation will eventually cause a GC spike. Target zero per-frame allocations.

Pre-Launch Mobile Optimization Checklist

Run through this checklist before every mobile build. It covers the highest-impact optimizations:

  • Target frame rate set (Application.targetFrameRate = 60 or 30)
  • Draw calls under 200 (check Stats window or Frame Debugger)
  • Static batching enabled for all non-moving objects
  • GPU Instancing enabled on shared materials
  • Texture compression set to ASTC (not uncompressed or RGBA32)
  • Texture sizes appropriate (no 4096x4096 textures on mobile sprites)
  • Physics fixed timestep set to 0.033 (30 Hz) or higher
  • Layer Collision Matrix configured — unused pairs disabled
  • No per-frame heap allocations in Update/LateUpdate/FixedUpdate
  • Object pooling used for frequently spawned/destroyed objects
  • UI split into static and dynamic canvases
  • Raycast Target disabled on non-interactive UI elements
  • URP mobile shaders used (no Standard/Legacy shaders)
  • Real-time lights limited to 2 or fewer
  • Audio clips compressed and set to appropriate load type
  • Profiled on the lowest-spec target device, not just the editor
  • Safe areas handled for notched devices — see our Mobile Safe Area script

For responsive UI scaling across different screen sizes and resolutions, our Mobile Responsive Scaler handles the heavy lifting. Combined with the Mobile Controls Kit, you'll have a solid foundation for any mobile project.

Optimization is iterative. Profile, identify the biggest bottleneck, fix it, and profile again. Don't guess — measure. A 1ms saving in a function that runs once per frame is worth more than a 10ms saving in a function that runs once per level load.