跪拜 Guibai
← All articles
iOS · Frontend

The iOS Render Loop, Frame by Frame

By _瑞 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

iOS frame drops are not one problem but two distinct failure modes—commit hitches and render hitches—that require different instruments and different fixes. Misdiagnosing a render hitch as a main-thread block, or vice versa, sends optimization effort to the wrong process entirely.

Summary

Every frame on iOS marches to a vertical sync beat through a five-stage pipeline: Event, Commit (Layout → Display → Prepare → Commit), Render Prepare, Render Execute, and Display. A hitch in any stage means a dropped frame. The pipeline is parallel—while the app prepares frame N, the render server processes N-1 and the display shows N-2—so the 16.7 ms budget is per-stage, not end-to-end.

Commit hitches happen when the app's main thread blocks on JSON parsing, synchronous I/O, lock contention, or when layout, text measurement, drawing, and image decoding overrun the deadline. Render hitches happen when the render server's CPU work (traversing the layer tree, computing animation states) or the GPU's pixel work (alpha blending, overdraw, off-screen rendering, large textures) can't finish in time.

Concrete fixes span the whole chain: pre-compute attributed strings and text layouts on background queues, throttle UI submission rates independently of data arrival, set explicit shadowPaths to avoid off-screen rendering, decode images to thumbnail dimensions before they hit the Prepare phase, and delete wrapper views that add no layout, clipping, or interaction responsibility. A companion open-source skill packages these rules so AI can audit a codebase for frame-drop risks directly.

Takeaways
VSync prevents screen tearing by blocking buffer swaps until the display finishes its current scan; on iOS it is always on and drives the entire render loop via CADisplayLink.
The render loop is a parallel pipeline: the app, render server, and display each work on different frames simultaneously, so a 16.7 ms budget is per-stage, not end-to-end.
Commit hitches mean the app failed to submit its layer tree on time; causes include main-thread JSON parsing, synchronous I/O, lock contention, Auto Layout churn, undecoded images, and deep layer trees.
Render hitches mean the render server or GPU missed its deadline; causes include complex layer effects, off-screen rendering from masks and dynamic shadows, alpha blending overdraw, and large textures.
UIImage creation does not decode the image; decoding happens lazily in the Commit transaction's Prepare phase, so large undecoded images spike that frame's cost.
Pre-computing attributed strings, YYTextLayout objects, and cell dimensions on background queues and storing them on the model removes text measurement from the main-thread Layout phase.
Throttling UI submission (e.g., 400 ms intervals) independently of data arrival prevents message floods from triggering a full layout-and-draw cycle per incoming item.
Setting an explicit shadowPath on a layer avoids the system re-inferring the shadow contour and can prevent off-screen rendering when combined with a separate inner layer for corner clipping.
Deleting wrapper views that contribute no layout, clipping, background, or interaction responsibility directly reduces the layer-tree node count the render server must traverse.
YYAsyncLayer uses an atomic incrementing sentinel value as a version number, checking it at multiple points during async drawing to prevent drawing onto a reused cell with stale content.
Conclusions

Apple's hitch taxonomy (commit vs. render) is underused in practice; most teams still talk vaguely about 'main thread' vs. 'GPU' without tracing the specific stage, which leads to fixing the wrong thing.

The advice to avoid Auto Layout in favor of manual frames is often premature—constraint reuse and scoped layoutIfNeeded calls solve most cases, and manual frames lose readability for marginal gain unless Instruments proves otherwise.

Off-screen rendering is not a single cause but a family of effects (masks, dynamic shadows, blurs, corner clipping with sublayers), and rounded corners alone do not guarantee it; the diagnostic tool output should drive the fix, not a blanket rule.

Async drawing adds scheduling overhead and dual-bitmap memory pressure; it is a tradeoff, not a universal upgrade, and the sentinel-based cancellation pattern in YYAsyncLayer is the critical detail that prevents drawing onto the wrong cell.

Preparing message display data on background queues (parsing, attributed string assembly, YYTextLayout) shifts work from both the Event and Layout phases, which is more impactful than optimizing either phase in isolation.

Concepts & terms
VSync (Vertical Sync)
A signal emitted by the display after finishing a full frame scan. It gates buffer swaps so the GPU never writes to the buffer the display is currently reading, preventing screen tearing. On iOS it is always enabled and drives CADisplayLink and Core Animation's commit cadence.
Commit Hitch
A frame drop caused by the app process failing to finish event handling and layer-tree submission (Layout → Display → Prepare → Commit) before its deadline. Diagnosed in Instruments under the Commits phase.
Render Hitch
A frame drop caused by the render server or GPU failing to complete Render Prepare or Render Execute on time, even though the app submitted its layer tree promptly. Diagnosed under the Renders or GPU phase in Instruments.
CATransaction
Core Animation's mechanism for batching layer-tree changes within a run-loop cycle. It collects property modifications and defers submission to the render server until the transaction commits, avoiding redundant intermediate work.
Backing Store
The per-layer bitmap memory that drawRect: paints into. It is not the screen buffer; it is the layer's own content storage, later read by the render server and GPU for compositing.
Off-Screen Rendering
When the GPU cannot composite a layer in a single pass and must first render intermediate results to a temporary buffer. Triggered by certain combinations of masks, shadows, corner clipping with sublayers, and blur effects. It adds a render pass and memory bandwidth cost.
YYSentinel
An atomic, increment-only counter used in YYAsyncLayer as a version number. Before and after async drawing, the sentinel value is compared to detect whether the content has changed or the cell has been reused, allowing cancellation of stale drawing tasks.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗