Unity mobile optimization: 12 ways to hit 60 FPS on low-end Android

A practical checklist for making Unity games run smoothly on budget phones: profiling, URP settings, batching, textures, garbage collection and build settings.

Guides By Leekon Studio 5 min read

A game that runs at 60 FPS on your development PC and a flagship phone can still stutter on the phones most players actually own. On Android especially, a large share of your audience is on budget devices with weak GPUs, little memory and aggressive thermal throttling.

This is the checklist we run on our own Unity games before they go anywhere near a store. It is ordered roughly by impact — start at the top.

1. Profile on a real low-end device first

Never optimise by guessing. Make a Development Build, install it on the cheapest phone you can find, and connect the Unity Profiler. The first question is always: is the CPU or the GPU the bottleneck? The fixes are completely different.

  • CPU-bound: scripts, physics, animation, UI rebuilds, draw-call submission.
  • GPU-bound: too many pixels, overdraw, heavy shaders, post-processing.

At 60 FPS you have ~16.6 ms per frame. On mobile, aim for 11–13 ms so the phone has headroom to stay cool.

2. Actually ask for 60 FPS

On Android and iOS, Unity defaults to 30 FPS unless you say otherwise, and vSyncCount is ignored on mobile. Set the target once at startup:

void Awake()
{
    Application.targetFrameRate = 60;
}

Also enable Optimized Frame Pacing in Player Settings → Android → Resolution and Presentation. It evens out frame delivery, so 60 FPS feels smooth instead of juddery.

3. Tune URP for mobile

If you are on the Universal Render Pipeline, create a dedicated low-end URP asset and assign it to your lowest quality level:

  • HDR off and MSAA off or 2x on low-end devices.
  • Render Scale 0.8–0.9 — a small drop in resolution is almost invisible on a phone screen and saves a lot of GPU time.
  • Depth Texture and Opaque Texture off unless an effect needs them.
  • Shadow distance as short as you can get away with, one cascade, and soft shadows off.
  • Keep post-processing to the essentials. Bloom and colour grading are usually affordable; depth of field and motion blur rarely are.

4. Cut draw calls and SetPass calls

Every change of mesh or material costs CPU time. Open the Frame Debugger and count.

  • Keep the SRP Batcher enabled (it is on by default in URP) and use SRP-compatible shaders.
  • Mark non-moving scenery as Static so static batching can combine it.
  • Use GPU instancing for many copies of the same mesh — trees, rocks, coins.
  • Share materials. Ten objects with ten slightly different materials cannot be batched together.
  • For 2D, pack sprites into Sprite Atlases so one draw call can render many sprites.

5. Compress textures properly

Textures are usually the biggest chunk of memory — and memory pressure is what crashes low-end phones.

  • Use ASTC compression. Choose 6x6 or 8x8 blocks for most textures and 4x4 only where detail matters, such as UI.
  • Set a sensible Max Size per texture. A 4K texture on a prop that covers 50 pixels on screen is pure waste.
  • Enable mipmaps for 3D textures (they improve both speed and quality at a distance), and disable them for UI.
  • Turn off Read/Write unless a script needs the pixels — otherwise the texture is kept in memory twice.

6. Fight overdraw

Low-end GPUs have limited fill rate: the number of pixels they can shade per second. Every transparent layer drawn on top of another costs you again.

  • Avoid large, full-screen transparent images and stacks of UI panels.
  • Keep particle systems small on screen and low in count — big, soft smoke particles are an overdraw trap.
  • Use URP’s Rendering Debugger overdraw view to see the hot spots.

7. Keep shaders simple

Prefer Simple Lit or Unlit shaders on mobile and save the full Lit shader for hero objects. In custom shaders, use half precision where you can, avoid expensive per-pixel maths, and strip unused shader variants to cut build size and load times.

8. Bake your lighting

Real-time lights and shadows are expensive on phones. Bake lightmaps for static scenery, use light probes to light moving objects cheaply, and keep real-time lights to a minimum — ideally one directional light.

9. Stop allocating garbage every frame

Garbage-collection spikes cause the classic “hitch every few seconds”. Open the Profiler’s GC Alloc column and hunt anything that allocates every frame:

  • String concatenation and formatting in Update (especially UI text updated every frame).
  • LINQ queries and lambdas that capture variables in hot code.
  • GetComponent or Find calls every frame — cache references in Awake.
  • Creating new arrays or lists each frame — reuse them instead.

Keep Incremental GC enabled in Player Settings to spread the remaining collection work across frames.

10. Pool what you spawn

Instantiating and destroying bullets, enemies or effects every second causes both CPU spikes and garbage. Unity ships a built-in pool (we walk through the object pool pattern in our design patterns guide):

using UnityEngine;
using UnityEngine.Pool;

public class BulletSpawner : MonoBehaviour
{
    [SerializeField] Bullet prefab;
    ObjectPool<Bullet> pool;

    void Awake()
    {
        pool = new ObjectPool<Bullet>(
            createFunc: () => Instantiate(prefab),
            actionOnGet: b => b.gameObject.SetActive(true),
            actionOnRelease: b => b.gameObject.SetActive(false),
            defaultCapacity: 32);
    }

    public Bullet Spawn() => pool.Get();
    public void Despawn(Bullet b) => pool.Release(b);
}

11. Keep UI and physics lean

UI: when any element on a Canvas changes, the whole Canvas may need rebuilding. Split UI into separate Canvases for static and frequently changing elements, disable Raycast Target on graphics that are not clickable, and avoid layout groups on elements that update every frame.

Physics: prefer primitive colliders (box, sphere, capsule) over mesh colliders, trim the Layer Collision Matrix so layers that never interact are not tested, and check that your Fixed Timestep is not running physics more often than you need.

12. Ship the right build settings

  • IL2CPP scripting backend with ARM64 — faster code, and 64-bit is required by Google Play anyway.
  • Compress audio: Vorbis for effects, Streaming load type for long music tracks, and Force To Mono where stereo adds nothing.
  • Consider the Adaptive Performance package to react to thermal state on supported devices.
  • Finally, play for 15 minutes, not 15 seconds. Many phones throttle once they heat up — the frame rate that matters is the one after the device gets warm.

The quick checklist

AreaCheck
ProfilingMeasured on a real low-end device, CPU vs GPU known
Frame ratetargetFrameRate = 60, Optimized Frame Pacing on
RenderingLow-end URP asset, render scale < 1, HDR off
Draw callsSRP Batcher, static batching, instancing, atlases
TexturesASTC, sensible max sizes, Read/Write off
GPUOverdraw checked, simple shaders, baked lighting
CPUZero per-frame GC allocations, pooling in place
UI & physicsCanvases split, collision matrix trimmed
BuildIL2CPP + ARM64, audio compressed, thermal-tested

Performance is a feature, and it is much cheaper when it is planned from the prototype onward than bolted on before launch. It also has a real effect on budget — see our breakdown of how much a mobile game costs to make.

If your Unity game is struggling on real devices, that is exactly the kind of work our LiveOps & porting team does. Send us a build and we’ll tell you where the milliseconds are going.

Frequently asked questions

On Android and iOS, Unity renders at 30 FPS by default when Application.targetFrameRate is left at -1. Set Application.targetFrameRate = 60 at startup to allow 60 FPS; on mobile, QualitySettings.vSyncCount is ignored.

60 FPS gives you about 16.6 milliseconds per frame for everything — scripts, physics, animation, rendering and the GPU. On phones, aim well below that (around 11–13 ms) so the device can stay cool and avoid thermal throttling.

For new mobile projects, generally yes. URP is designed to scale down to low-end hardware, has the SRP Batcher enabled by default, and exposes per-quality settings like render scale, HDR, MSAA and shadow distance that make tuning for cheap devices much easier.

ASTC is the best default for modern Android devices. Use larger block sizes (6x6 or 8x8) for most textures to save memory, and smaller ones (4x4) only for UI or textures where detail really matters.

Build a Development Build, run it on a real low-end phone, and connect the Unity Profiler. Check whether the CPU or GPU is the bottleneck first, then use the Frame Debugger to count draw calls and the GC Alloc column to find per-frame allocations.

Written by

Leekon Studio

An indie Unity game studio in Islamabad building original games for PC, mobile and web — and helping other teams ship theirs. About the studio.

Enjoyed this?

Let's make a game.

Prototype, full production, or a strike team inside yours — we reply within two working days.

Start a project