Use when app drains battery, device gets hot, users report energy issues, or auditing power consumption - systematic Power Profiler diagnosis, subsystem identification (CPU/GPU/Network/Location/Display), anti-pattern fixes for iOS/iPadOS
Instrucciones de origen · Vista previa de solo lectura
name
axiom-energy
description
Use when app drains battery, device gets hot, users report energy issues, or auditing power consumption - systematic Power Profiler diagnosis, subsystem identification (CPU/GPU/Network/Location/Display), anti-pattern fixes for iOS/iPadOS
license
MIT
metadata
{"version":"1.0.0"}
Energy Optimization
Overview
Energy issues manifest as battery drain, hot devices, and poor App Store reviews. Core principle: Measure before optimizing. Use Power Profiler to identify the dominant subsystem (CPU/GPU/Network/Location/Display), then apply targeted fixes.
Key insight: Developers often don't know where to START auditing. This skill provides systematic diagnosis, not guesswork.
Requirements: iOS 26+, Xcode 26+, Power Profiler in Instruments
Example Prompts
Real questions developers ask that this skill answers:
1. "My app is always at the top of Battery Settings. How do I find what's draining power?"
→ The skill covers Power Profiler workflow to identify dominant subsystem and targeted fixes
2. "Users report my app makes their phone hot. Where do I start debugging?"
→ The skill provides decision tree: CPU vs GPU vs Network diagnosis with specific patterns
3. "I have timers and location updates. Are they causing battery drain?"
→ The skill covers timer tolerance, location accuracy trade-offs, and audit checklists
4. "My app drains battery in the background even when users aren't using it."
→ The skill covers background execution patterns, BGTasks, and EMRCA principles
5. "How do I measure if my optimization actually improved battery life?"
→ The skill demonstrates before/after Power Profiler comparison workflow
Red Flags — High Energy Likely
If you see ANY of these, suspect energy inefficiency:
Battery Settings: Your app consistently at top of battery consumers
Device temperature: Phone gets warm during normal app use
User reviews: Mentions of "battery drain", "hot phone", "kills my battery"
Xcode Energy Gauge: Shows sustained high or very high impact
Background runtime: App runs longer than expected when not visible
Network activity: Frequent small requests instead of batched operations
Location icon: Appears in status bar when app shouldn't need location
Difference from normal energy use
Normal: App uses energy during active use, minimal when backgrounded
Problem: App uses significant energy even when user isn't interacting
Mandatory First Steps
ALWAYS run Power Profiler FIRST before optimizing code:
Step 1: Record a Power Trace (5 minutes)
1. Connect iPhone wirelessly to Xcode (wireless debugging)
2. Xcode → Product → Profile (Cmd+I)
3. Select Blank template
4. Click "+" → Add "Power Profiler" instrument
5. Optional: Add "CPU Profiler" for correlation
6. Click Record
7. Use your app normally for 2-3 minutes
8. Click Stop
Why wireless: When device is charging via cable, power metrics show 0. Use wireless debugging for accurate readings.
Step 2: Identify Dominant Subsystem
Expand the Power Profiler track and examine per-app metrics:
Lane
Meaning
High Value Indicates
CPU Power Impact
Processor activity
Computation, timers, parsing
GPU Power Impact
Graphics rendering
Animations, blur, Metal
Display Power Impact
Screen usage
Brightness, always-on content
Network Power Impact
Radio activity
Requests, downloads, polling
Look for: Which subsystem shows highest sustained values during your app's usage.
Step 3: Branch to Subsystem-Specific Fixes
Once you identify the dominant subsystem, use the decision trees below.
What this tells you
CPU dominant → Check timers, polling, JSON parsing, eager loading
Network dominant → Check request frequency, polling vs push
Display dominant → Check Dark Mode, brightness, screen-on time
Location (shown in CPU) → Check accuracy, update frequency
Why diagnostics first
Finding root cause with Power Profiler: 15-20 minutes
Guessing and testing random optimizations: 4+ hours, often wrong subsystem
Energy Decision Tree
User reports energy issue?
│
├─ CPU Power Impact dominant?
│ ├─ Continuous high impact?
│ │ ├─ Timers running? → Pattern 1: Timer Efficiency
│ │ ├─ Polling data? → Pattern 2: Push vs Poll
│ │ └─ Processing in loop? → Pattern 3: Lazy Loading
│ ├─ Spikes during specific actions?
│ │ ├─ JSON parsing? → Cache parsed results
│ │ ├─ Image processing? → Move to background, cache
│ │ └─ Database queries? → Index, batch, prefetch
│ └─ High background CPU?
│ ├─ Location updates? → Pattern 4: Location Efficiency
│ ├─ BGTasks running too long? → Pattern 5: Background Execution
│ └─ Audio session active? → Stop when not playing
│
├─ Network Power Impact dominant?
│ ├─ Many small requests?
│ │ └─ Batch into fewer large requests
│ ├─ Polling pattern detected?
│ │ └─ Convert to push notifications → Pattern 2
│ ├─ Downloads in foreground?
│ │ └─ Use discretionary background URLSession
│ └─ High cellular usage?
│ └─ Defer to WiFi when possible
│
├─ GPU Power Impact dominant?
│ ├─ Continuous animations?
│ │ └─ Stop when view not visible
│ ├─ Blur effects (UIVisualEffectView)?
│ │ └─ Reduce or remove, use solid colors
│ ├─ High frame rate animations?
│ │ └─ Audit secondary frame rates → Pattern 6
│ └─ Metal rendering?
│ └─ Implement frame limiting
│
├─ Display Power Impact dominant?
│ ├─ Light backgrounds on OLED?
│ │ └─ Implement Dark Mode (up to 70% savings)
│ ├─ High brightness content?
│ │ └─ Use darker UI elements
│ └─ Screen always on?
│ └─ Allow screen to sleep when appropriate
│
└─ Location causing drain? (check CPU lane + location icon)
├─ Continuous updates?
│ └─ Switch to significant-change monitoring
├─ High accuracy (kCLLocationAccuracyBest)?
│ └─ Reduce to kCLLocationAccuracyHundredMeters
└─ Background location?
└─ Evaluate if truly needed → Pattern 4
Common Energy Patterns (With Fixes)
Pattern 1: Timer Efficiency
Problem: Timers wake the CPU from idle states, consuming significant energy.
❌ Anti-Pattern — Timer without tolerance
// BAD: Timer fires exactly every 1.0 seconds// Prevents system from batching with other timersTimer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _inself.updateUI()
}
✅ Fix — Set tolerance for timer batching
// GOOD: 10% tolerance allows system to batch timerslet timer =Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _inself.updateUI()
}
timer.tolerance =0.1// 10% tolerance minimum// BETTER: Use Combine Timer with toleranceTimer.publish(every: 1.0, tolerance: 0.1, on: .main, in: .default)
.autoconnect()
.sink { [weakself] _inself?.updateUI()
}
.store(in: &cancellables)
✅ Best — Use event-driven instead of polling
// BEST: Don't use timer at all — react to eventsNotificationCenter.default.publisher(for: .dataDidUpdate)
.sink { [weakself] _inself?.updateUI()
}
.store(in: &cancellables)
Key points:
Set tolerance to at least 10% of interval
Timer tolerance allows system to batch multiple timers into single wake
Prefer event-driven patterns over polling timers
Always invalidate timers when no longer needed
Pattern 2: Push vs Poll
Problem: Polling (checking server every N seconds) keeps radios active and drains battery.
❌ Anti-Pattern — Polling every 5 seconds
// BAD: Polls server every 5 seconds// Radio stays active, massive battery drainTimer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weakself] _inself?.fetchLatestData() // Network request every 5 seconds
}
✅ Fix — Use background push notifications
// GOOD: Server pushes when data changes// Radio only active when there's actual new data// 1. Register for remote notificationsUIApplication.shared.registerForRemoteNotifications()
// 2. Handle background notificationfuncapplication(_application: UIApplication,
didReceiveRemoteNotificationuserInfo: [AnyHashable: Any],
fetchCompletionHandlercompletionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
guardlet_= userInfo["content-available"] else {
completionHandler(.noData)
return
}
Task {
do {
let hasNewData =tryawait fetchLatestData()
completionHandler(hasNewData ? .newData : .noData)
} catch {
completionHandler(.failed)
}
}
}
Background pushes are discretionary — system delivers at optimal time
Use apns-priority: 5 for non-urgent updates (energy efficient)
Use apns-priority: 10 only for time-sensitive alerts
Polling every 5 seconds uses 100x more energy than push
Pattern 3: Lazy Loading & Caching
Problem: Loading all data upfront causes CPU spikes and memory pressure.
❌ Anti-Pattern — Eager loading (from WWDC25-226)
// BAD: Creates and renders ALL views upfront// From WWDC25-226: This caused CPU spike and hangVStack {
ForEach(videos) { video inVideoCardView(video: video) // Creates ALL thumbnails immediately
}
}
✅ Fix — Lazy loading
// GOOD: Only creates visible views// From WWDC25-226: Reduced CPU power impact from 21 to 4.3LazyVStack {
ForEach(videos) { video inVideoCardView(video: video) // Creates on-demand
}
}
// BAD: Parses JSON file on every location update// From WWDC25-226: Caused continuous CPU drain during commutefuncvideoSuggestionsForLocation(_location: CLLocation) -> [Video] {
// Called every location change!let data =try?Data(contentsOf: rulesFileURL)
let rules =try?JSONDecoder().decode([RecommendationRule].self, from: data)
return filteredVideos(using: rules)
}
✅ Fix — Cache parsed data
// GOOD: Parse once, reuse cached result// From WWDC25-226: Eliminated CPU drainprivatelazyvar cachedRules: [RecommendationRule] = {
let data =try?Data(contentsOf: rulesFileURL)
return (try?JSONDecoder().decode([RecommendationRule].self, from: data)) ?? []
}()
funcvideoSuggestionsForLocation(_location: CLLocation) -> [Video] {
return filteredVideos(using: cachedRules) // No parsing!
}
Key points:
Use LazyVStack, LazyHStack, LazyVGrid for large collections
Cache parsed JSON, decoded data, computed results
Move expensive operations out of frequently-called methods
// BAD: Continuous updates with best accuracy// GPS stays active constantly, massive battery drainlet locationManager =CLLocationManager()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation() // Never stops!
✅ Fix — Appropriate accuracy and significant-change
// GOOD: Reduced accuracy, significant-change monitoringlet locationManager =CLLocationManager()
// Use appropriate accuracy (100m is fine for most apps)
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
// Use distance filter to reduce updates
locationManager.distanceFilter =100// Only update every 100 meters// For background: Use significant-change monitoring
locationManager.startMonitoringSignificantLocationChanges()
// Stop when donefuncstopTracking() {
locationManager.stopUpdatingLocation()
locationManager.stopMonitoringSignificantLocationChanges()
}
✅ Better — iOS 26+ CLLocationUpdate with stationary detection
// BEST: Modern async API with automatic stationary detectionfortryawait update inCLLocationUpdate.liveUpdates() {
if update.stationary {
// Device stopped moving — system pauses updates automatically// Switch to CLMonitor for region monitoringbreak
}
handleLocation(update.location)
}
Accuracy comparison (battery impact):
Accuracy
Battery Impact
Use Case
kCLLocationAccuracyBest
Very High
Navigation apps only
kCLLocationAccuracyNearestTenMeters
High
Fitness tracking
kCLLocationAccuracyHundredMeters
Medium
Store locators
kCLLocationAccuracyKilometer
Low
Weather apps
Significant-change
Very Low
Background updates
Pattern 5: Background Execution (EMRCA)
Problem: Background tasks that run too long or too often drain battery.
Resilient — Save incremental progress; respond to expiration signals
Courteous — Honor user preferences and system conditions
Adaptive — Understand and adapt to system priorities
❌ Anti-Pattern — Long-running background task
// BAD: Requests unlimited background time// System will terminate after ~30 seconds anywayvar backgroundTask: UIBackgroundTaskIdentifier= .invalid
funcapplicationDidEnterBackground(_application: UIApplication) {
backgroundTask = application.beginBackgroundTask {
// Expiration handler — but task runs too long
}
// Long operation that may not complete
performLongOperation()
}
✅ Fix — Proper background task handling
// GOOD: Finish quickly, save progress, notify systemvar backgroundTask: UIBackgroundTaskIdentifier= .invalid
funcapplicationDidEnterBackground(_application: UIApplication) {
backgroundTask = application.beginBackgroundTask(withName: "Save State") { [weakself] in// Expiration handler — clean up immediatelyself?.saveProgress()
iflet task =self?.backgroundTask {
application.endBackgroundTask(task)
}
self?.backgroundTask = .invalid
}
// Quick operation
saveEssentialState()
// End task as soon as done — don't wait for expiration
application.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
✅ For Long Operations — Use BGProcessingTask
// BEST: Let system schedule at optimal time (charging, WiFi)funcscheduleBackgroundProcessing() {
let request =BGProcessingTaskRequest(identifier: "com.app.maintenance")
request.requiresNetworkConnectivity =true
request.requiresExternalPower =true// Only when chargingtry?BGTaskScheduler.shared.submit(request)
}
// Register handler at app launchBGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.app.maintenance",
using: nil
) { task inself.handleMaintenance(task: task as!BGProcessingTask)
}
✅ iOS 26+ — BGContinuedProcessingTask for user-initiated work
// NEW iOS 26: Continue user-initiated tasks with progress UIlet request =BGContinuedProcessingTaskRequest(
identifier: "com.app.export",
title: "Exporting Photos",
subtitle: "23 of 100 photos"
)
try?BGTaskScheduler.shared.submit(request)
Pattern 6: Frame Rate Auditing
Problem: Secondary animations running at higher frame rates than needed increase GPU power.
❌ Anti-Pattern — Uncontrolled frame rates
// BAD: Secondary animation runs at 60fps// When primary content only needs 30fps, this wastes powerUIView.animate(withDuration: 2.0, delay: 0, options: [.repeat]) {
self.subtitleLabel.alpha =0.5
} completion: { _inself.subtitleLabel.alpha =1.0
}
From WWDC22-10083: Up to 20% battery savings by aligning secondary animation frame rates with primary content.
Audit Checklists
Timer Audit
All timers have tolerance set (≥10% of interval)?
Timers invalidated when no longer needed?
Using Combine Timer instead of NSTimer where possible?
No polling patterns that could use push notifications?
Timers stopped when app enters background?
Network Audit
Requests batched instead of many small requests?
Using discretionary URLSession for non-urgent downloads?
waitsForConnectivity set to avoid failed connection attempts?
allowsExpensiveNetworkAccess set to false for deferrable work?
Push notifications instead of polling?
Location Audit
Using appropriate accuracy (not kCLLocationAccuracyBest unless navigation)?
distanceFilter set to reduce update frequency?
Stopping updates when no longer needed?
Using significant-change for background updates?
Background location justified and explained to users?
Background Execution Audit
endBackgroundTask called promptly when work completes?
Long operations use BGProcessingTask with requiresExternalPower?
Background modes in Info.plist limited to what's actually needed?
Audio session deactivated when not playing?
EMRCA principles followed?
Display/GPU Audit
Dark Mode supported (70% OLED power savings)?
Animations stopped when view not visible?
Secondary animations use appropriate frame rates?
Blur effects minimized or removed?
Metal rendering has frame limiting?
Disk I/O Audit
Writes batched instead of frequent small writes?
SQLite using WAL journaling mode?
Avoiding rapid file creation/deletion?
Using SwiftData/Core Data instead of serialized files for frequent updates?
Pressure Scenarios
Scenario 1: "Just poll every 5 seconds for real-time updates"
The temptation: "Push notifications are complex. Polling is simpler."
The reality:
Polling every 5 seconds: Radio active 100% of time
Push notifications: Radio active only when data changes
Users WILL see your app at top of Battery Settings
App Store reviews WILL mention "battery hog"
Time cost comparison:
Implement polling: 30 minutes
Implement push: 2-4 hours
Fix bad reviews + reputation damage: Weeks
Pushback template: "Push notification setup takes a few hours, but polling will guarantee we're at the top of Battery Settings. Users actively uninstall apps that drain battery. The 2-hour investment prevents ongoing reputation damage."
Scenario 2: "Use continuous location for best accuracy"
The temptation: "Users expect accurate location. Let's use kCLLocationAccuracyBest."
kCLLocationAccuracyHundredMeters: Good enough for 95% of use cases
Location icon in status bar = users checking Battery Settings
Time cost comparison:
Implement high accuracy: 10 minutes
Debug "why does my app drain battery" complaints: Hours
Refactor to appropriate accuracy: 30 minutes
Pushback template: "100-meter accuracy is sufficient for [use case]. Navigation apps like Google Maps need best accuracy, but we're showing [store locations / weather / general area]. The accuracy difference is imperceptible to users, but battery difference is massive."
The temptation: "Animations make the app feel alive and polished."
The reality:
Animations running when view not visible = pure waste
High frame rate secondary animations = GPU drain
GPU power is significant portion of total device power
Time cost comparison:
Add animation: 15 minutes
Add visibility checks: 5 minutes extra
Debug "phone gets hot" reports: Hours
Pushback template: "We can keep the animation, but should pause it when the view isn't visible. This is a 5-minute change that prevents GPU drain when users aren't looking at the screen."
Scenario 4: "Ship now, optimize later"
The temptation: "Energy optimization is polish. We can do it in v1.1."
The reality:
Battery drain is immediately visible to users
First impressions drive reviews
"Battery hog" reputation is hard to shake
Power Profiler baseline takes 15 minutes
Time cost comparison:
Power Profiler check before launch: 15 minutes
Fix energy issues post-launch: Days (plus reputation damage)
Regain user trust: Months
Pushback template: "A 15-minute Power Profiler session before launch catches major energy issues. If we ship with battery problems, users will see us at top of Battery Settings on day one and leave 1-star reviews. Let me do a quick check — it's faster than damage control."
Real-World Examples
Example 1: Video Streaming App with Eager Loading (WWDC25-226)
Symptom: CPU power impact jumped from 1 to 21 when opening Library pane. UI hung.
Diagnosis using Power Profiler:
Recorded trace while opening Library pane
CPU Power Impact lane showed massive spike
Time Profiler showed VideoCardView body called hundreds of times
Root cause: VStack creating ALL video thumbnails upfront