Optimization Best Practices
How professional teams approach performance: set budgets, measure on real hardware, find whether you're CPU- or GPU-bound, fix the biggest problem first, and keep it from coming back. This page covers the process and the engine-level settings. For script-level tips, see Optimizing Your Code.
A household budget. You don't save money by randomly skipping coffee. You write down how much you can spend (the frame budget), track where the money actually goes (profiling), cut the biggest expense first, and check your statement every month so it doesn't creep back up (regular profiling).
1. Decide your targets early
Performance is much cheaper to plan for than to fix at the end. Before production, write down:
| Target | Example |
|---|---|
| Frame rate per platform | PC 60 FPS (16.6 ms), Switch-class 30 FPS (33.3 ms), mobile 30 or 60 FPS |
| Minimum-spec device | "A 4-year-old mid-range Android phone" |
| Memory budget | Stay well below what the weakest device allows, leaving room for the OS |
| Load time | "Any level loads in under 10 seconds" |
| Rough per-system split (example) | Of 16.6 ms: gameplay scripts 3 ms, physics 2 ms, animation 2 ms, rendering 6 ms, headroom 3 ms |
Aim below the limit. A game that takes exactly 16.6 ms per frame drops frames whenever anything extra happens. Leave headroom for spikes like explosions and loading.
2. Profile on the target hardware
- The Editor is slower and behaves differently than a build. Make a Development Build with Autoconnect Profiler and profile the real device.
- Profile the weakest device you support, not your development PC.
- On mobile, test for 20-30 minutes. Phones get hot and thermal throttling lowers CPU/GPU speed, so a game that runs well for 2 minutes may not at minute 15.
- Profile the heaviest moments (big fights, busy areas), not an empty test scene.
| Tool | Use it to find |
|---|---|
| Profiler (Window > Analysis) | Where CPU time goes each frame; GC allocations; rendering, physics, audio, memory overview |
| Profile Analyzer (package) | Compare many frames, or before/after a change, statistically |
| Memory Profiler (package) | What's using memory: textures, meshes, leaked objects |
| Frame Debugger | Every draw call and why objects didn't batch |
| Platform tools | Xcode Instruments (iOS), Android GPU Inspector, RenderDoc, PIX: deep GPU analysis |
3. Find out what's limiting you
Fixing the wrong side wastes time. Figure out what you're bound by first:
| Bound by | Signs | Typical fixes |
|---|---|---|
| CPU: scripts | Profiler shows your Update methods, BehaviourUpdate or coroutines taking most of the frame | Cache, do less per frame, pooling, Jobs + Burst |
| CPU: rendering | Lots of time preparing draw calls on the main/render thread; high Batches/SetPass counts | Batching, instancing, fewer materials, LODs, culling |
| CPU: physics | Physics.Simulate / FixedUpdate is large | Simpler colliders, layer matrix, larger fixed timestep |
| GPU | The CPU spends time waiting for the GPU (markers such as Gfx.WaitForPresentOnGfxThread); lowering resolution raises FPS | Lower resolution/render scale, cheaper shaders and post-processing, less overdraw and fewer shadows |
| Garbage collector | Regular spikes labeled GC.Collect | Remove per-frame allocations; enable incremental GC |
4. Measure, change one thing, measure again
- Record a baseline in a repeatable situation (same scene, same camera path).
- Change one thing.
- Record again and compare (the Profile Analyzer is ideal).
- Keep it only if it helped. Write down what you changed and the result.
Add your own markers so your systems show up clearly in the Profiler:
using Unity.Profiling;
using UnityEngine;
public class EnemyAI : MonoBehaviour
{
private static readonly ProfilerMarker ThinkMarker = new ProfilerMarker("EnemyAI.Think");
void Update()
{
using (ThinkMarker.Auto()) // everything in this block is timed under "EnemyAI.Think"
{
Think();
}
}
void Think() { /* ... */ }
}
5. Architecture choices that scale
- Event-driven over polling. React when something changes instead of checking every frame.
- Do less, less often. AI doesn't need to think 60 times a second; far-away objects don't need full updates. See Update & FixedUpdate for staggering.
- Pool frequently spawned objects. See Object Pooling.
- Data-oriented code for big numbers. Thousands of units? Store data in arrays of structs and process them in tight loops. Unity's C# Job System spreads work over CPU cores, and the Burst compiler turns that code into highly optimized machine code. For very large simulations, Unity's Entities (ECS) package is built around this.
- Draw without GameObjects where possible (see GPU Instancing).
- Spread spikes over frames ("time slicing"): generate a level chunk per frame rather than everything in one frame, and load scenes asynchronously.
6. Assets and memory
| Asset | Good practice |
|---|---|
| Textures | Use platform compression (e.g. ASTC on mobile). Set Max Size to what's actually visible. Mipmaps on for 3D, off for UI. Atlases for UI and 2D sprites. |
| Meshes | Turn off Read/Write Enabled unless scripts read the mesh (saves a CPU-side copy). Use LODs. Compress meshes if precision allows. |
| Audio | Music: Streaming. Short, frequent sounds: Decompress On Load. Everything else: Compressed In Memory. Mono for 3D sounds. |
| Animation | Compress clips; set Animators' Culling Mode so off-screen characters don't update fully. |
| Loading | Use Addressables to load and unload content on demand. Avoid the Resources folder for large content: everything in it is packed into the build and indexed at startup. |
7. Engine and build settings
- Incremental GC (Player Settings, on by default in recent versions): spreads garbage collection over several frames to avoid big spikes. It doesn't replace reducing allocations.
- IL2CPP scripting backend for release builds: converts C# to C++ for faster code (required on iOS). Combine with Managed Stripping to reduce build size.
- Target frame rate on mobile: mobile platforms default to 30 FPS. Set
Application.targetFrameRate = 60;if you want 60, or keep 30 to save battery and heat. - Physics: set the Layer Collision Matrix, keep Auto Sync Transforms off, and choose the largest fixed timestep that still feels right.
- Quality levels: provide Low/Medium/High presets (shadow distance, render scale, post-processing) and pick a default per device.
- Strip logs from release builds, and don't ship Development Builds.
8. Keep it fast
- Profile regularly, not just before release. A weekly check catches problems while it's still obvious which change caused them.
- Automate: the Performance Testing package (
com.unity.test-framework.performance) can record timings in tests, so a CI build can flag regressions. - Performance is part of "done". A feature that breaks the frame budget isn't finished.
- Don't micro-optimize blindly. Readable code that's fast enough beats clever code that's slightly faster. Optimize what the Profiler points at.
Checklist
- Targets and budgets written down per platform.
- Profiled a development build on the minimum-spec device, including a long session.
- Know whether you're CPU-bound (which part?) or GPU-bound.
- No per-frame GC allocations in gameplay.
- Frequently spawned objects are pooled.
- Draw calls checked in the Frame Debugger; materials shared; instancing where it helps.
- Texture, audio and mesh import settings reviewed.
- Release build: IL2CPP, no dev logs, correct target frame rate.
See also
This page is a simplified, AI-assisted interpretation. For the complete and authoritative reference, see the official Unity documentation.