Quick answer: A single changing element marks the whole Canvas dirty — the entire batch rebuilds. Put dynamic elements (timers, health bars) on their own Canvas, separate from static UI.
The Profiler shows a Canvas.SendWillRenderCanvases / rebuild spike every frame. A constantly-updating score text is dirtying the whole HUD canvas.
One Dirty Element = Whole Canvas Rebuild
UGUI batches a Canvas’s geometry. Change any element on it — text, position, color — and the whole Canvas regenerates its batches. A per-frame timer text rebuilds your entire HUD every frame.
Split by Update Frequency
- Static Canvas: backgrounds, frames, labels that never change. Rebuilds ~never.
- Dynamic Canvas: score, timer, health bar. Rebuilds often, but it’s small and cheap.
A nested Canvas component creates a separate batch boundary — changes inside it don’t dirty the parent.
Other Wins
- Disable Raycast Target on non-interactive Images/Text — cuts graphic raycaster cost.
- Avoid animating
RectTransformon a big shared canvas; isolate it. - For text that changes every frame, TextMeshPro is cheaper than legacy Text.
Verifying
Profiler: the rebuild spike now only covers the small dynamic canvas. The static HUD canvas shows zero rebuilds during gameplay. Frame time drops and steadies.
“Canvases rebuild as a unit. Separate ‘changes every frame’ from ‘never changes’.”
Rule of thumb: if it animates or counts, it gets its own Canvas. Everything else can share one static canvas.