Unity Mobile Performance: 12 Fixes That Buy You 20 FPS on Low-End Android
Twelve fixes that reliably lift frame rate on cheap Android hardware, in the order we apply them, starting with how to find the actual bottleneck before changing a single setting.

Frame rate on low-end Android is usually lost in six places: draw calls, texture memory, overdraw, the physics step, garbage collection, and UI canvas rebuilds. Fixing all six adds up, and on a typical mobile project the total gain lands in the range this title promises. Find your bottleneck first.
Why this matters
The devices that decide your retention are not the ones on your desk. A large share of installs in most markets land on hardware that is several years old and thermally limited.
A game that runs at 60 on a flagship and 22 on a budget phone loses those players in the first session. They rarely tell you why.
The good news is that mobile performance work is unusually predictable. The same handful of causes come up again and again.
Every number below is additive and device-dependent. Treat them as where to look, not as a guaranteed result on your build.
Find the bottleneck before you fix anything
Changing settings without measuring is how a week disappears.
Open the Unity Profiler, connect to a real device, and answer one question: is the frame CPU-bound or GPU-bound?
- CPU-bound shows a long main thread with scripts, physics, or rendering setup dominating.
- GPU-bound shows the main thread waiting, usually visible as a large gap or a render thread that never catches up.
- Memory-bound shows up as stalls and spikes rather than a consistently low frame rate.
Then use the Frame Debugger to see what the renderer is actually submitting. Most surprises live there.
Draw calls and batching
Every draw call costs CPU time on the submission side, and low-end chips have very little of it to spare.
- Share materials. Two objects with the same material and no per-object properties can batch. Two with different material instances cannot.
- Use a sprite or texture atlas so unrelated objects stop breaking the batch.
- Turn on GPU instancing for anything repeated many times, such as props and pickups.
- Enable static batching for geometry that never moves.
- Watch MaterialPropertyBlock use, which can silently break batching when applied broadly.
Look for the SetPass Calls counter rather than the raw draw call count. That is the number that hurts.
Texture memory and compression formats
Texture memory is the most common cause of crashes and stutter on cheap devices.
Use ASTC on Android where it is supported, and set a sane maximum size per texture rather than shipping everything at source resolution.
- Drop UI and background textures to the smallest size that still looks right on a phone screen.
- Turn off mipmaps for UI, which never needs them.
- Turn off Read/Write Enabled unless something genuinely reads the pixels, because it doubles memory.
- Check crunch compression for large atlases where load time matters more than quality.
Overdraw and transparent shaders
Overdraw is the quiet GPU killer, and it is almost always caused by transparency.
Every full-screen transparent layer costs you a full screen of shading. Stack four of them and the GPU is doing four times the work for one visible result.
- Replace large transparent quads with opaque geometry wherever the art allows.
- Cut particle counts and particle size before you cut particle quality.
- Use the overdraw view in the Scene window to find the worst offenders quickly.
- Avoid full-screen post-processing on low-end tiers entirely.
Physics step and fixed timestep
Physics runs on its own clock, and the default is often wrong for mobile.
A fixed timestep of 0.02 means fifty physics steps per second whatever your frame rate. On a device struggling to hit 30, that is a large fixed tax.
- Raise the fixed timestep to match the rate you actually need, commonly 0.0333.
- Set Maximum Allowed Timestep low so a hitch cannot trigger a death spiral of catch-up steps.
- Simplify colliders. Mesh colliders on moving objects are expensive; primitives are close to free.
- Trim the collision matrix so layers that never interact stop being tested.
Garbage collection spikes
A stutter every few seconds is usually the collector, not the renderer.
Allocation in Update is the root cause almost every time.
- Cache component lookups instead of calling GetComponent per frame.
- Avoid string concatenation in per-frame code, especially in UI updates.
- Use non-allocating physics calls such as the NonAlloc raycast variants.
- Pool anything you create and destroy repeatedly, which we cover in Object Pooling in Unity.
- Watch for foreach over certain collection types in older runtimes, and for boxing in event calls.
UI canvas rebuilds
Unity UI rebuilds an entire canvas when anything on it changes. A single animated counter can rebuild a screen of static elements sixty times a second.
- Split canvases so moving elements sit on their own canvas, away from static ones.
- Turn off Raycast Target on every element that is not interactive, which is most of them.
- Replace layout groups with fixed positions once a layout has settled.
- Keep text updates off the per-frame path where the value has not changed.
There is more detail in Unity UI That Does Not Tank Your Frame Rate.
The settings checklist for a low-end build
Work down this list before shipping a build to a cheap device.
- Set a target frame rate explicitly rather than leaving it to the platform.
- Turn off VSync in the quality settings and control pacing yourself.
- Disable real-time shadows on the low tier, or reduce shadow distance sharply.
- Bake lighting wherever the scene is static.
- Reduce the pixel light count to one, or zero for stylised art.
- Use a lower render scale rather than a lower resolution asset set.
- Strip unused shader variants, which also improves load time.
- Set the scripting backend to IL2CPP and enable managed stripping.
- Turn off accelerometer polling if you do not read it.
- Cap the particle raycast budget.
- Check that the audio is compressed and streamed sensibly.
- Profile again on the real device, because the list above changes what the bottleneck is.
What we would do
Pick two physical test devices early and keep them on the desk: one cheap Android phone that represents your floor, and one mid-tier device that represents the bulk of your users. Emulators will not tell you the truth about thermal behaviour.
Then work in the order above, measuring after each change. Draw calls and overdraw usually pay first. Garbage collection pays the most for perceived smoothness, because stutter reads as broken in a way a steady lower frame rate does not.
Set a performance budget in week one rather than week twenty. A frame budget written down is a design constraint everyone can work inside.
The short version
- Profile on a real device and decide CPU-bound or GPU-bound before touching settings.
- SetPass calls, texture memory, and overdraw are the three usual GPU costs.
- Fixed timestep and collider complexity are the usual physics costs.
- Per-frame allocation causes the stutter people actually notice.
- Canvas splitting and Raycast Target are the cheapest UI wins available.
- Measure after every change, because the bottleneck moves.
Set your device floor first, then work the list. If your build is struggling and you would rather have someone else find it, send us the project and we will profile it.
Related reading: Cut Your Unity Build Size in Half, Fixing Physics Jitter in Unity Mobile Games, and Shipping on Android.
Got a game idea? We build it.
You bring the concept. We design, build, test and launch it, and you own 100% of the finished game.
Share Your Game Idea →