Mobile performance budget
Assets/
Scripts/
Settings/
Project settings
you already havethis lesson adds
You already hit a batch and triangle budget for the Arena in tab 04 — on a desktop. A mid-range phone has roughly a tenth of that GPU, and it slows itself down further once it heats up, after about five minutes.
Four numbers to cut, one number to raise.
The mobile budget
Draw calls / batches: under 100 is comfortable, under 60 is safe on weak devices.
Triangles on screen: 100k–300k. Above that, mid-range GPUs start to sweat.
Realtime lights: one directional, and that is it. Everything else baked.
Texture memory: under 200 MB. Check it in the Profiler's Memory module, not by guessing.
Target: a stable 60 FPS, or a rock-solid 30 if the device cannot hold 60.
1. URP settings — the biggest single win
- 1
Find your URP Asset ( shows which one is active).
- 2
Shadows → Max Distance 40, Cascade Count 1, Shadow Resolution 1024.
- 3
Untick HDR and untick MSAA on the mobile URP asset. Both are expensive on tile-based mobile GPUs.
- 4
Set Render Scale to 0.8 on low-end devices — a 20% resolution cut is nearly invisible and gives a huge speedup.
- 5
Keep separate URP assets per quality level: Mobile Low, Mobile High, Desktop.
2. Textures — usually half your build size
- 1
Select all textures, set Max Size 1024 (512 for props, 2048 only for hero assets).
- 2
Compression Format → ASTC 6x6 for Android. It is the modern standard and looks far better than ETC2 at the same size.
- 3
Untick Read/Write Enabled unless you genuinely read pixels in code — it doubles memory usage.
- 4
Turn off mipmaps for UI sprites; keep them on for anything in 3D space.
3. Scripts — the mistakes that cost frames
void Update()
{
// 1. searching the whole scene, 60 times a second
GameObject player = GameObject.Find("Player");
// 2. allocating a new object every frame
Enemy[] all = FindObjectsOfType<Enemy>();
// 3. building a string every frame — this is garbage
label.text = "Score: " + score;
// 4. a component lookup every frame
GetComponent<Rigidbody>().AddForce(Vector3.up);
}private Transform player;
private Rigidbody rb;
private int lastScore = -1;
void Awake()
{
player = GameObject.FindWithTag("Player").transform; // once
rb = GetComponent<Rigidbody>();
}
void Update()
{
rb.AddForce(Vector3.up);
if (score != lastScore) // only build the string when it changed
{
label.text = "Score: " + score;
lastScore = score;
}
}Garbage collection is what causes the periodic hitch every few seconds on mobile. Almost all of it comes from strings and new allocations in Update.
Anything that does not need to run at 60 Hz should run on a timer — an enemy AI check every 0.2 s is 12 times cheaper.
Empty Update() methods still cost a call from the engine. Delete them.
4. Detect the device and scale down
using UnityEngine;
public class QualityAutoSetter : MonoBehaviour
{
void Awake()
{
int ram = SystemInfo.systemMemorySize; // in MB
int cores = SystemInfo.processorCount;
if (ram < 3000 || cores <= 4)
{
QualitySettings.SetQualityLevel(0, true); // Mobile Low
Application.targetFrameRate = 30;
}
else if (ram < 6000)
{
QualitySettings.SetQualityLevel(1, true); // Mobile Medium
Application.targetFrameRate = 60;
}
else
{
QualitySettings.SetQualityLevel(2, true); // Mobile High
Application.targetFrameRate = 60;
}
Debug.Log($"Device: {SystemInfo.deviceModel}, RAM {ram}MB, quality {QualitySettings.GetQualityLevel()}");
}
}Profile on the device, not in the editor
The editor runs on your desktop CPU. Its numbers tell you nothing about a phone.
Tick Development Build and Autoconnect Profiler in Build Profiles, then build to the device and open the Profiler.
Play for five full minutes. Thermal throttling means minute five is 20–30% slower than minute one — that is the number that matters.