Power and Performance Group Lab

Join us online for a deep dive into WWDC26 with Apple engineers and designers to ask questions, get advice, and follow the discussion about the week's biggest power and performance announcements. Conducted in English.

Transcript

Engineers from the performance and power teams introduce themselves and set up a session covering SwiftUI performance, energy usage and battery, Instruments, background tasks, MetricKit and the Xcode Organizer, and managing thermal pressure.

The biggest lever in SwiftUI is minimizing unnecessary redraws: separate your views from their inputs so a view only observes the state it actually depends on. When updates happen, you don't want to redraw content that didn't change — scoping observation tightly keeps CPU work down and avoids both lag and battery drain.

There's no single culprit, but the most common one is insufficient telemetry and instrumentation. Without it you optimize the wrong things. Instrument your app to understand which features and scenarios users actually exercise, add metrics around them, and focus effort where it delivers the biggest return.

The Instruments tutorials are the best starting point — written by an Instruments engineer to feel like having one beside you, walking through the app step by step. They ship with an associated project that has performance issues built in, so you can practice detecting hangs and other problems hands-on.

This refers to Xcode's energy gauges. High background energy despite simple screens usually means the app is scheduling background work you may not be accounting for. Investigate which modes of your app schedule tasks, and profile with Instruments' energy/CPU tooling to pinpoint the actual source rather than trusting the gauge summary alone.

There's no fixed number — it depends on the views involved. Environment is a genuinely useful abstraction for passing values down the view tree. The concern is invalidation scope: if the theme object changes, everything observing it redraws. Keep frequently-changing values out of a broadly-observed object, and split state so a change only invalidates the components that truly depend on it.

Load data lazily and scalably rather than materializing everything at once, so SwiftUI only builds visible rows. For measuring, MetricKit gives you aggregated field metrics (hang rate, CPU, memory) from real users; establish baselines and watch those signals across releases to catch regressions, complementing local Instruments profiling.

The system balances background work against everything else running, including intelligence features. Don't assume a background processing or refresh task will run immediately or at a fixed time — design them to be resilient: express your work through the background task APIs, handle deferral and rate limiting gracefully, and retry later rather than depending on guaranteed execution windows.

Each thread hop adds switching overhead, so doing it very frequently is a real cost — but occasional hops are fine and far preferable to blocking the main thread. Rather than hopping per small task, batch work onto a background context and hop back once with results. Profile with Instruments to see whether hopping or contention is actually your launch bottleneck.

View over-invalidation is a major silent drain: repeatedly recreating views that look identical burns CPU with no visible change, which is exactly why it's easy to miss. Use the SwiftUI Instruments template to spot views updating more than they should, then narrow observation so each view depends only on the state it renders.

Help SwiftUI by keeping row sizing predictable — constant or known row heights let it manage a large list efficiently, whereas continually changing sizes forces expensive relayout. Combined with lazy containers so only visible rows are built, this keeps long lists and tables smooth at scale.

AnyView adds some overhead at view-creation time because it erases type information SwiftUI would otherwise use to diff efficiently. Avoid it where it's easy to, but don't contort your architecture around it — it's the kind of thing you can address if profiling shows it's actually a problem, rather than optimizing away preemptively.

The Xcode Organizer is worth attention — like MetricKit it surfaces field data, but aggregated across your whole user population with visualizations, giving a different, broader view of real-world performance and power than per-session captures.

Hands-on experience is what builds real expertise. Rather than only reading, work with the Instruments tools across many scenarios — the range of views and perspectives they provide teaches you how the system actually behaves. Profile real code, form and test hypotheses, and build intuition from measurement over time.

Where you can, call a closure in the view's init rather than in its body, so it doesn't re-run every time the view is re-evaluated — this avoids the cost that made escaping closures problematic while still letting you pass content in. Architect so expensive closure work happens once at construction, not on every body evaluation.

Right-size your assets to the use case — don't load a 2000×2000 image for a thumbnail; use appropriately scaled versions. Combined with minimizing view invalidation and doing heavy work off the main thread, tailoring asset sizes keeps update-driven CPU spikes and hitches down, which matters most on older hardware.

Observe the system's thermal state API and adapt as it rises — scale back non-essential work, reduce frame rate or rendering fidelity, and defer background processing when the device enters higher thermal states. Designing graceful degradation tied to thermal state keeps the core experience responsive rather than letting the system throttle you abruptly.

The key factor is environment churn, not the number of values. Simply putting values into the environment and reading them in your views is inexpensive. The cost appears when those values update at high frequency: every view reading from the environment then has to re-evaluate whether what it depends on changed. So a deep hierarchy with many injected values is fine on its own — problems start when the environment churns frequently. The SwiftUI Instrument lets you see this in practice.