Performance guide¶
Tick UI profiling and optimisation centres on three things: where draw-call cost lives, how to read the profiler markers, and what to change when the frame budget is tight.
Where the cost lives¶
Two phases dominate per-frame cost:
- Recording. Your
DoUIruns every frame. Layout pushes, widget calls, and per-shape command construction happen here. - Playback. The drawer's
RecordIntowalks the recorded commands. Batchable shapes (rectangles, segments, shapes) are grouped into instanced calls; the rest issue oneDrawMesheach.
Most panel-heavy UIs are CPU-bound on the recording side. GPU cost stays low until you reach hundreds of frosted-glass shapes per frame.
For the model behind both phases, refer to Drawer and batching.
Reading the profiler¶
Open Unity's Profiler, switch to the CPU module, and filter on the markers in the Profiler markers reference.
The most useful filters when starting a profile:
| Marker | What it tells you |
|---|---|
UI.BeginFrame |
Per-frame cost of input update, panel resolution, scale-cache refresh. |
UI.EndFrame |
Per-frame cost of panel-layer flush and stack validation. |
Drawer.Frame |
Per-frame cost of text-mesh cache prune and the rest of Drawer.Frame(). |
Drawer.HandleCommands |
Total playback cost for the recorded commands. |
Handle Draw Frosted |
Cost attributable to frosted shapes (often the worst offender). |
Drawer.DrawText |
Mesh-build cost on the first frame of each new string. |
Common hot spots¶
Text-mesh creation.
: Each unique (string, font, size, align, wrap) tuple builds
one mesh on first use, then caches. The first frame after a
string changes pays the build cost. Cache the string upstream
when possible, or accept the one-frame spike.
Frosted glass. : Each frosted shape costs one full-target blit plus one multi-tap shader sample. Group frosted regions where possible.
Repeated layout pushes inside a hot loop.
: Each push runs LayoutToScreen and stack manipulation.
Pulling repeated values out of the loop helps:
var rowH = ui.PeekHeight() / rowCount;
foreach (var row in rows)
using (ui.Auto().Top(rowH)) { /* draw */ }
Animation contexts opened per row.
: Each PushAnimate allocates an entry on the animation stack.
For a long list, profile the animation overhead and consider
sharing a context when the visual permits.
Mitigations¶
The following mitigations reduce per-frame cost:
- Cache layout-derived values. Compute measured layouts once per frame, then pass them down.
- Lift work out of
DoUI. Anything that does not depend on per-frame state belongs inStartor a one-shot setup method. - Disable off-screen rows. Scroll views cull naturally. Flat long lists do not, so skip drawing rows whose rect falls outside the visible area.
- Audit allocations. A per-frame
new List<>()adds per-frame GC pressure. Reuse buffers stored on theBaseGameUIsubclass.
Order calls for instancing¶
The instancing pass groups consecutive same-shape commands into a single batched call. The recording side stays the same. Two implications follow:
- Order calls so same shape types run adjacent. Issuing all rectangles before all text within a panel produces one batch for rectangles and one mesh per text string. Mixed orders break the rectangle batch into smaller runs.
- Avoid breaking the batch. Clip-rect changes and frosted shapes flush the current batch.
For the batching model, refer to Drawer and batching.
Frame-budget rule of thumb¶
On desktop hardware, a HUD-scale UI (a few panels, a toolbar, a dozen animated buttons) keeps the CPU under 1 ms per frame. A settings or inventory screen with hundreds of items costs more, so profile under realistic load.
Mobile and console targets need tighter budgets. Profile on the target device early and often.
Additional resources¶
- Drawer and batching: the model behind the cost.
- Profiler markers: the full list of markers to filter on.
- State: per-frame caching patterns.
- Frame lifecycle: which phase each marker covers.