Walk through how a frame gets from your code to the screen.
Predict first
A frame takes 20 ms to prepare on the main thread at 60 Hz. What does the user see?
// A: a smooth frame, because the GPU is fast
// B: a hitch: the budget is about 16.7 ms, so the previous frame is shown again
// C: nothing is shown
- A
- B
- C
Mental model. The app is a stage manager writing the scene description and handing it through a hatch to a crew in another room, who build and light the set. A late description or a complicated set both make the curtain rise late.
A frame has two halves. In your process, the Core Animation commit happens at the end of the current run-loop turn (a run-loop observer drives it). It has four phases: layout (layoutSubviews, constraints solved), display (draw(_:) runs for views that need it, producing bitmaps for their backing stores), prepare (images are decoded and converted if they were not already; this is why a huge JPEG can stall the first frame it appears in) and commit (the layer tree and its changes are packaged and sent to the render server over IPC). Setting a property just records a change; nothing is drawn until the commit.
The render server (a separate system process, backboardd/the render server) receives the transaction, applies any animations, renders the layers with the GPU (compositing them in order with their opacity, masks, shadows and transforms) and presents the result at the next display refresh (vsync). Animations that Core Animation owns (position, opacity, transform) are run there, so they stay smooth even if your main thread is busy; an animation you drive from your own code (a timer, CADisplayLink) depends on the main thread making every frame. A frame has a budget (about 16.7 ms at 60 Hz, 8.3 ms at 120 Hz) covering both halves, and missing it shows the previous frame again, a hitch. So when a frame is slow, ask: was the CPU part slow (layout, drawing, decoding, your own code on the main thread), or the GPU part (too many offscreen passes, large blended areas, heavy effects)? The Core Animation instrument and the debug options answer that.
The budget covers the whole pipeline. Exceeding it means the new frame misses the display refresh, so the old one stays on screen.
Common traps
- Assuming changing a property redraws immediately. It records the change and the commit applies it, at the end of the run-loop turn.
- Blaming the GPU for every hitch. Most are main-thread work (layout, decode, your code) before the commit.
They might follow up with
- Why do Core Animation animations survive a busy main thread? The render server runs them, not your app.
- What does the prepare phase do? Decodes and converts images, which is why a large image can stall the frame that first shows it.
Related questions
- 9.2.2 What are the model, presentation and render layer trees, and how do the layer geometry properties relate?
- 9.7.1 What is a hitch versus a hang, how does ProMotion change the budget, and how do you measure and fix them?
- 6.3.1 What is a run loop, and what happens in one iteration?
What are the model, presentation and render layer trees, and how do… →