CPU, GPU or memory?
A slow Unity game usually has one limit at a time. Find which one before you optimize, or the fix may change nothing.
Find the side that’s waiting.
The CPU prepares each frame while the GPU draws. The slower of the two sets your frame rate. Change the work on each side and watch the budget.
The solid line is the frame budget. The dashed line is the mobile target with headroom. The striped segment is the extra time on a garbage-collection frame.
Within budget: the GPU needs 12.0 ms and the CPU 9.0 ms, inside 16.7 ms.
Frame 12.0 ms · 60 fps on a 60 Hz screen with VSync · budget 16.7 ms
A browser illustration, not a Unity measurement. The values are model inputs. It assumes the CPU and GPU work in parallel, VSync on a 60 Hz screen and GPU work proportional to the number of pixels.
Try it.
- Raise GPU work to 22 ms, then lower CPU work. The frame time does not move. The GPU is the limit, so CPU savings are invisible.
- Now lower the resolution scale. GPU time falls with the pixel count, and the frame comes back inside the budget.
- Set GPU work to 17 ms. The frame misses 16.7 ms by a fraction, and VSync shows it at 30 fps, not 59.
- Put GPU work back to 12 ms and add an 8 ms garbage-collection pause. Most frames still fit, but every collection is a visible hitch.
- Turn on mobile headroom. The usable budget drops to about 11 ms at 60 fps.
Start with the frame budget.
At 60 fps, every frame has 16.7 ms. At 30 fps, it has 33.3 ms. The CPU runs gameplay, physics, animation and UI, then prepares the draw commands. The GPU turns them into pixels. When either side runs over, the frame is late.
Phones need extra room. Without active cooling, a phone running at full load heats up, and the system slows the CPU and GPU down. Unity’s profiling guide suggests using only about 65% of the budget on mobile: about 11 ms at 60 fps and 22 ms at 30 fps.
Memory works differently. It rarely makes every frame slower. It shows up as hitches, long loading times and crashes.
Find out which side is waiting.
Start with a quick test on the target device: lower the render resolution. If the frame rate rises, the GPU is the limit. If nothing changes, look at the CPU.
Then confirm it in the Unity Profiler, with a development build on the device rather than in the Editor:
- Gfx.PresentFrame on the render thread: time spent waiting for the GPU to render and present the frame, including VSync. If it is large while frames miss the budget, the game is GPU-bound.
- Gfx.WaitForCommands on the render thread: it is ready for work, waiting for the main thread. The game is CPU-bound.
- WaitForTargetFPS: the frame finished early and is waiting for the target frame rate. That time is headroom.
CPU: too much work, or work at the wrong time.
The usual causes are scripts that run every frame on every object, physics with too many active bodies, UI canvases that rebuild when one element changes, and too many draw calls to submit. Batching, such as the SRP Batcher or GPU instancing, reduces that last cost.
Garbage collection deserves its own check. Every managed allocation appears as GC.Alloc in the Profiler. When the heap needs space, GC.Collect pauses your code. Incremental garbage collection spreads that work over several frames, but the better fix is to stop allocating every frame: pool objects, reuse collections and avoid building strings in Update.
From my projects. On Critical Strike CS, low-end Android went from under 20 fps to a steady 60 fps, and per-frame garbage dropped from 100+ KB to near zero.
GPU: too many pixels, or too much per pixel.
GPU time grows with the number of pixels and the work done for each one. Common causes are high resolution, overdraw from layers of transparent particles, UI and foliage, expensive shaders and post-processing, and real-time shadows and lights.
Dynamic resolution or a lower render scale reduces the pixel count. The Frame Debugger shows every draw call in a frame, which helps you spot what draws the same area many times. On mobile, test long sessions too: a scene that holds 60 fps for one minute may not hold it once the phone is warm.
Memory: the problem you notice last.
Textures are often the largest share. Use sizes that match how the texture appears on screen, compressed formats for the platform and mipmaps where the camera needs them. Assets stay in memory while something references them, so loading screens are a good place to check what never leaves.
Addressables let you load content when you need it and release it afterwards. On phones with little RAM, the system can close a game that uses too much memory, often without a useful error. The Memory Profiler package lets you take snapshots before and after a level and compare them.
From my projects. At JustPlay / Gimica, a host app loads and unloads games on demand with Addressables, cutting download size by over 90%.
Fix in the right order.
- Measure on the target device, with a development build, in the heaviest scene.
- Find the limiting side, then the biggest cost on that side.
- Change one thing and measure again.
- Keep headroom for the worst moment, not the average one.
When the CPU is the limit because of scale, with thousands of units or a large simulation, data-oriented design can change the picture. Measure first: Before rewriting a Unity system in ECS shows how to decide.
From my projects. On Grey Eminence, after the move to DOTS, the full game loop ran roughly 1000× faster.
Go deeper.
Unity · Best practices for profiling game performance Unity 6 Manual · Common Profiler markers Unity 6 Manual · Frame Debugger Unity 6 Manual · Incremental garbage collection Unity · Memory Profiler packageBy Jack Mariani. Need help with a slow game? Explore Performance & Optimization.