What is offscreen rendering, which properties cause it, and how do you avoid the cost?
Predict first
What do these defaults read, and does a corner radius alone clip?
import UIKit
let l = CALayer()
print(l.shouldRasterize, l.rasterizationScale, l.shadowPath == nil, l.masksToBounds)
let cv = UIView()
cv.layer.cornerRadius = 8
print(cv.layer.masksToBounds)
cv.clipsToBounds = true
print(cv.layer.masksToBounds)
- false 1.0 true false, then false, then true
- true 1.0 true false, then true, then true
- false 1.0 false false, then false, then true
Predict first
A scrolling list of cards each with a rounded corner, a shadow and a photo. Which change usually gives the biggest saving?
// A: set a shadowPath and pre-render the rounded photo
// B: turn on shouldRasterize for every cell
// C: add more layers
- A
- B
- C
Mental model. Painting directly on the wall is quick. Offscreen rendering is painting the picture on a separate canvas, cutting it to shape, and then sticking it on the wall, every single frame.
Normally the GPU draws each layer straight into the frame buffer in order. Offscreen rendering means a layer (and often its whole subtree) must first be rendered into a separate buffer, then processed, then drawn into the frame: extra memory, an extra pass and a context switch for the GPU on every frame. Typical triggers are a mask (layer.mask), cornerRadius combined with masksToBounds when the layer has content or subviews (note: a radius alone only rounds the background and border and does not clip), a shadow without a shadowPath (the system has to render the layer to learn its shape), group opacity (allowsGroupOpacity, when a view with subviews has alpha below 1 and the group must be flattened), shouldRasterize (the layer is cached as a bitmap), and some blur and visual-effect views. One layer is rarely a problem; hundreds in a scrolling list are.
Fixes follow the cause. Give shadows an explicit shadowPath (a path of the shape, such as UIBezierPath(roundedRect:)), so the shadow can be drawn without measuring. For rounded images, pre-render the rounded image once (a UIGraphicsImageRenderer with a clip path, cached) or use a corner-masking style that the system can do cheaply (cornerCurve with images, or UIButton configurations) instead of clipping live. Avoid masks on scrolling content; use opaque views and avoid transparency where you can. shouldRasterize helps only for a static, expensive subtree that is drawn repeatedly: it caches the result (at rasterizationScale, which you must set to the screen scale or the layer looks blurry) but throws the cache away when the content changes, and it adds an offscreen pass itself, so enabling it on changing content makes things slower. shouldRasterize and drawsAsynchronously default to false. Verify with the Color Offscreen-Rendered Yellow debug option in the simulator, and the Core Animation instrument, rather than assuming.
Rasterization and clipping are off by default, a shadow has no explicit path until you give one, and a radius alone does not set masksToBounds.
It removes the per-frame offscreen passes. Rasterizing changing cells adds a pass and a cache rebuild.
Common traps
- Turning on
shouldRasterizefor content that changes every frame. The cache is rebuilt each time, so it only adds cost. - Setting a shadow and relying on the layer’s content for its shape. Without a
shadowPathevery frame needs an extra pass to find the outline.
They might follow up with
- Does
cornerRadiusalone force an offscreen pass? No, it only rounds the background and border; clipping contents withmasksToBoundsis what can. - When is
shouldRasterizea win? For a static, expensive subtree drawn many times, with the correctrasterizationScale.
Related questions
- 9.3.2 What are blending and overdraw, how do opaque views help, and what are dirty rectangles?
- 9.2.1 Walk through how a frame gets from your code to the screen.
- 7.5.3 How does cell reuse work, and what are the classic reuse bugs?
What are blending and overdraw, how do opaque views help, and what… →