A small scrolling ticker pins GPU at ~50% on a rich screen but ~0% on a blank one — is whole-tree compositing at the animation's rate the expected cost?

I have a SwiftUI clock-radio app on iPhone (ProMotion display). The face is a rich, layered screen (glass panels, gradients, shadows). One element is a 340×46 pt text ticker strip that scrolls continuously. Its GPU behaviour is reproducible and, to me, surprising:

Measurements (Xcode GPU gauge used only for within-session relative contrasts; cadence from an in-app frame-pacing log):

  • With the ticker scrolling, GPU sits at a ~50% plateau. The step is binary: one scroller or three scrollers cost the same.
  • Frames are never dropped: 16.7 ms median frame time, ~3% of frames over 20 ms. The GPU bills heavily for frames it renders on time.
  • The same strip scrolling on an otherwise blank screen costs ~0%.
  • The ticker visible but static costs 0%. Motion is the trigger; the cost scales with what surrounds the strip, not with the strip itself.

What I have tried:

  • SwiftUI offset animation of the text — baseline behaviour above.
  • A fully severed Canvas leaf: own layer subtree, own CADisplayLink, snapshot-fed, no view-tree publishing or subscriptions — identical cost on the full face (and literally 0% redrawing alone on a blank screen at full frame rate).
  • Pre-rendered bitmap content scrolled instead of text — identical.
  • The strip hosted in a separate UIWindow above the face — identical, measured with the strip visibly scrolling in its seat.
  • Pinning the strip's CADisplayLink.preferredFrameRateRange to exactly 30 Hz (minimum = maximum = preferred = 30) — no change.
  • .compositingGroup() on the panel subtree — a modest reduction only.

What works: removing the motion duty (scroll once, then park — cost collapses), and, elsewhere in the app, a pull-model spectrum visualiser replaced a publish-driven one and its cost vanished. Severing worked for the visualiser but not for the ticker.

Questions:

  1. Is this step the expected consequence of the WWDC22 session 10083 rule (the display refresh rate is set by the highest-frame-rate animation in the app) composed with whole-layer-tree compositing per presented frame? Is there any dirty-region scoping that a layer structure like ours defeats?
  2. The 30 Hz pin above was hard (min = max = preferred). Would a CAFrameRateRange in the 15–24 Hz band that Apple's ProMotion guidance suggests for small, slow animations behave materially differently, or is that distinction already settled by the 30 Hz result?
  3. Is there a known construction for a continuously scrolling ticker that lets the GPU idle between updates?

Happy to share a minimal repro or the pacing logs if useful.

Answered by DTS Engineer in 915516022

Thanks for the careful contrasts: blank face against rich face, static against moving, and one scroller against three. Your numbers tie the cost to the motion and to what surrounds the strip, rather than to the strip itself.

The documentation doesn't describe how much GPU work a given layout costs per frame. I can't tell you whether 50% is the expected cost for your face. Optimizing iPhone and iPad apps to support ProMotion displays describes the refresh rate as "the rate that Core Animation renders the content for the entire display." Improving your app's rendering efficiency says to "Avoid view layouts where updating one layer requires the system to redraw other overlapping layers." It adds that "the system performs additional calculations to update the Liquid Glass appearance when your app updates screen regions around the Liquid Glass effect." It also says that grouping spatially distant views in a GlassEffectContainer "can cause unnecessary updates." To see which regions the ticker's motion updates, choose Debug > View Debugging > Rendering > Flash Updated Regions in Xcode while your app runs.

On your second question, the ProMotion article's table lists CAFrameRateRange(minimum:15, maximum:24, preferred:0) among the hints for small, low-speed animations, with a ticking clock among its examples. The article also says that Core Animation "determines the refresh rate at any particular time." It adds that "you can't force a ProMotion display to show your content at any specific rate." It doesn't say whether a 15 to 24 Hz range costs less than your 30 Hz pin. Measuring both on your device is the way to find out.

On your third question, I didn't find a documented construction for a continuously scrolling ticker that lets the GPU idle between updates. The rendering article says to "pause or stop" animations "when the update completes," which matches what you found when the ticker scrolls once and parks.

Please file a bug report in Feedback Assistant with the minimal repro you offered, your pacing logs, and the device model and iOS version. Post the Feedback number here, and I'll check its status next time I do a sweep of forums posts where I've suggested bug reports.

A repro example would be helpful, if you're willing to file feedback with it. I'd be curious how the SwiftUI route you’re using compares to using Core Animation on its own, i.e. putting your scrollable content into a layer with a looping CABasicAnimation applied to its position or transform. Given what you mention about how it behaves differently depending on the content around it, I wonder if the cost comes from nearby glass elements needing to pick up parts of the UI that the scrolling content affects… but a test project should hopefully show that either way.

Thanks for the careful contrasts: blank face against rich face, static against moving, and one scroller against three. Your numbers tie the cost to the motion and to what surrounds the strip, rather than to the strip itself.

The documentation doesn't describe how much GPU work a given layout costs per frame. I can't tell you whether 50% is the expected cost for your face. Optimizing iPhone and iPad apps to support ProMotion displays describes the refresh rate as "the rate that Core Animation renders the content for the entire display." Improving your app's rendering efficiency says to "Avoid view layouts where updating one layer requires the system to redraw other overlapping layers." It adds that "the system performs additional calculations to update the Liquid Glass appearance when your app updates screen regions around the Liquid Glass effect." It also says that grouping spatially distant views in a GlassEffectContainer "can cause unnecessary updates." To see which regions the ticker's motion updates, choose Debug > View Debugging > Rendering > Flash Updated Regions in Xcode while your app runs.

On your second question, the ProMotion article's table lists CAFrameRateRange(minimum:15, maximum:24, preferred:0) among the hints for small, low-speed animations, with a ticking clock among its examples. The article also says that Core Animation "determines the refresh rate at any particular time." It adds that "you can't force a ProMotion display to show your content at any specific rate." It doesn't say whether a 15 to 24 Hz range costs less than your 30 Hz pin. Measuring both on your device is the way to find out.

On your third question, I didn't find a documented construction for a continuously scrolling ticker that lets the GPU idle between updates. The rendering article says to "pause or stop" animations "when the update completes," which matches what you found when the ticker scrolls once and parks.

Please file a bug report in Feedback Assistant with the minimal repro you offered, your pacing logs, and the device model and iOS version. Post the Feedback number here, and I'll check its status next time I do a sweep of forums posts where I've suggested bug reports.