Highlight
Power Profiler now runs without Xcode. It captures power traces directly on the device, covering battery problems that only show up during commutes, on CarPlay, or outdoors.
Main Content
Many power bugs share one symptom: the app runs fine on the engineer’s desk and drains the battery once it reaches users. Wiam gave her own example in the talk. Her teammates on the Destination Video app complained that it had been topping the battery rankings for a long time, but she could not reproduce the problem no matter what she tried. The reason was simple. Her teammates used the app on their commute, with the location changing constantly. She sat at her desk, where the location barely moved, so the code path that drained the battery never ran.
The fix at WWDC25 is to move Power Profiler out of Xcode and onto the device itself. Developers can start a Performance Trace right on the iPhone, hand it to a QA engineer or beta user for a few hours, then transfer the trace file to a Mac and open it in Instruments. The whole flow needs no cable and no Xcode in the room. It covers the scenarios that were hardest to reproduce before: CarPlay navigation, outdoor AR use, long background drain. Combined with the desktop Power Profiler template (system-level power, plus power impact for the CPU, GPU, Display, and Network subsystems, plus Time Profiler), developers now have a full chain of power evidence that runs from the desk to the field.
Detailed Content
The talk covers four concrete scenarios: a reproducible problem, a non-reproducible problem, comparing solutions, and proactive monitoring.
Scenario one: a reproducible CPU spike (02:24). Wiam added a Library pane to the app. The Xcode Organizer energy report flagged a CPU spike right away. She picked the blank template in Instruments, added Power Profiler and CPU Profiler, and profiled the app over a wireless connection. On the CPU power impact lane, the average before opening the Library pane was 1, and it jumped to 21 after opening it (05:20). The Heaviest Stack Trace in Time Profiler pointed straight at VideoCardView. LibraryThumbnailView was using a VStack to build thousands of video cards in one pass.
The fix is to swap VStack for LazyVStack (07:30):
// Before: Create all VideoCardView instances at once
struct LibraryThumbnailView: View {
let videos: [Video]
var body: some View {
ScrollView {
VStack {
ForEach(videos) { video in
VideoCardView(video: video)
}
}
}
}
}
// After: Render only the views in the visible area
struct LibraryThumbnailView: View {
let videos: [Video]
var body: some View {
ScrollView {
LazyVStack {
ForEach(videos) { video in
VideoCardView(video: video)
}
}
}
}
}
Key points:
VStackbuilds and keeps every child view as soon as the body is evaluated. The longer the list, the higher the launch cost and memory footprint.LazyVStackonly builds views for items currently visible or about to scroll into the viewport, and discards them once they scroll off.- The data source
videosdoes not change. The view tree shape does not change. Only the container type changes. That is exactly why SwiftUI ships Lazy containers. - After the fix, the same window’s average CPU power impact dropped from 21 to 4.3 (08:32).
Scenario two: on-device capture (09:27). Path to enable: Settings -> Developer -> Performance Trace -> turn on the Power Profiler switch -> pick the target app (limited to apps installed via Xcode, TestFlight, or an enterprise certificate). Then swipe down from the top right to open Control Center, add the Performance Trace icon, and tap to start. You can record for hours, then tap again to stop. AirDrop the trace file to a Mac and open it in Instruments. The Time Profiler sampling rate in the on-device mode is lower, to reduce the observer effect.
Wiam used a trace sent back by a teammate to pin down videoSuggestionsForLocation, which was eating a lot of CPU time (13:32). This function recommends nearby videos based on location, and gets called every time the location changes. The buggy code reread the large RecommendationRules JSON file and decoded it from scratch on every call:
// Problem: reread the file and fully parse the JSON on every location update
func videoSuggestionsForLocation(_ location: CLLocation) -> [Video] {
let url = Bundle.main.url(forResource: "RecommendationRules", withExtension: "json")!
let data = try! Data(contentsOf: url)
let rules = try! JSONDecoder().decode([RecommendationRule].self, from: data)
return filter(videos: allVideos, with: rules, near: location)
}
// Optimization: rules do not change at runtime, so lazy-load and cache them
private static let recommendationRules: [RecommendationRule] = {
let url = Bundle.main.url(forResource: "RecommendationRules", withExtension: "json")!
let data = try! Data(contentsOf: url)
return try! JSONDecoder().decode([RecommendationRule].self, from: data)
}()
func videoSuggestionsForLocation(_ location: CLLocation) -> [Video] {
return filter(videos: allVideos, with: Self.recommendationRules, near: location)
}
Key points:
- File I/O and JSON parsing inside a high-frequency callback are a textbook power trap. The bigger the JSON and the more often the location updates, the worse the impact.
- Lifting
rulesinto astatic letwith an immediately executed closure is equivalent to a thread-safe lazy load. Swift guarantees that aletstatic property initializes only once. - After the fix, ask the teammate to record another trace under the same commute scenario for comparison, to confirm the bug is gone and no new power regression has been introduced (15:51).
Scenario three: comparing solutions (16:19). Given two candidate implementations A and B, A might use less CPU on small data while B saves more network on large data. Reading the code alone cannot tell you the net win. Profile each one separately, run multiple times and take the average, hold thermal state, device state, system pressure, and in-app feature flags constant, then compare side by side.
Scenario four: defense in depth (18:25). Apple’s official advice is to chain the power tools into a pipeline: while writing code, watch Xcode Energy Gauges for live feedback -> when you suspect a problem, dig in with Instruments Power Profiler -> in CI, run XCTests to catch regressions automatically -> after shipping, use Xcode Organizer and MetricKit to watch real users -> use the App Store Connect API to pull aggregated remote data.
Key Takeaways
- Run a Power Profiler trace right now: plug in a device, profile your main user paths, and check whether CPU/GPU power impact has unexpected spikes. This is the cheapest power audit you can do. Finding nothing is also a good outcome.
- Default to LazyVStack/LazyHStack instead of VStack/HStack: unless the data set is clearly under 20 items, use Lazy containers for lists and grids. This was the 21 -> 4.3 fix in Wiam’s case, with almost zero code cost.
- Use on-device Performance Trace for dogfooding and QA: have teammates record traces during real commutes, outdoors, and on CarPlay, and send them back. Bake this into your beta testing playbook. It will surface power bugs that never reproduce at the desk.
- Never do file I/O or JSON parsing inside high-frequency callbacks like location updates, timers, or notifications. The most direct fix is to cache immutable data with a
static letclosure for lazy loading. - Record a comparison trace after every performance change: confirm the fix and confirm no new regression. Power Profiler’s comparison method requires holding thermal state and device state constant, with multiple runs averaged.
Related Sessions
- Optimize CPU performance with Instruments — the matching CPU optimization method, recommended directly in this talk.
- Finish tasks in the background — power management for background tasks, complementary to this talk.
- Get ahead with quantum-secure cryptography — the power impact of cryptographic migrations, which you can verify with Power Profiler.
- Filter and tunnel network traffic with NetworkExtension — a representative case for diagnosing Network subsystem power issues.
- Deliver age-appropriate experiences in your app — a design reference for system services sessions.
Comments
GitHub Issues · utterances