iosNudge

How do you make an iOS app energy efficient?

MediumPerformance 2 min read

TL;DR

Energy is used by the CPU, GPU, network radio, location hardware and the display. Rules: do less, do it later, do it together. Batch network calls, use the lowest location accuracy you need, avoid polling and frequent timers, and let the system schedule background work. Measure with the Xcode energy gauge, the Power Profiler and MetricKit.

1. Network: batch and wait

Waking the cellular radio costs a lot. The radio also stays on for some seconds after each request.

// Bad: one request per event, every few seconds
func track(_ event: Event) async { try? await api.send(event) }

// Better: collect and send in batches
actor EventBatcher {
    private var pending: [Event] = []
    func add(_ e: Event) async {
        pending.append(e)
        if pending.count >= 20 { await flush() }
    }
    func flush() async {
        let batch = pending
        pending = []
        try? await api.send(batch)
    }
}

For non-urgent transfers, use a background session and let the system pick the time:

let config = URLSessionConfiguration.background(withIdentifier: "com.example.sync")
config.isDiscretionary = true // waits for Wi-Fi and power when possible

Also: cache responses, compress payloads, and avoid polling. Use push notifications for updates.

2. Location: as little as possible

let manager = CLLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyKilometer   // not Best
manager.distanceFilter = 500                              // meters
  • Use significant location changes or region monitoring when you do not need live tracking.
  • Stop updates as soon as you have what you need.
  • In iOS 17+, CLLocationUpdate.liveUpdates() gives an AsyncSequence. Stop iterating to stop updates.
for try await update in CLLocationUpdate.liveUpdates() {
    if let location = update.location {
        save(location)
        break // stop as soon as we have one good fix
    }
}

3. CPU: no busy work

  • Avoid timers that fire often. If you need one, add tolerance so the system can group wake-ups.
let timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { _ in refresh() }
timer.tolerance = 10
  • Stop animations, timers and observers when the screen is not visible or the app is in the background.
  • Do not poll for changes in a loop. Use notifications, KVO, AsyncSequence or Observation.
  • Use the right task priority. .background and .utility work can run on efficiency cores.

4. GPU and display

  • Avoid constant animations and needless redraws.
  • Dark UI saves power on OLED screens.
  • Do not render at 120 Hz when you do not need it. CADisplayLink lets you set preferredFrameRateRange.

5. Background work

  • Use BGTaskScheduler, not long-running background modes.
  • Set requiresExternalPower = true for heavy BGProcessingTask work.
  • Always call endBackgroundTask and setTaskCompleted quickly.

6. Respect Low Power Mode

if ProcessInfo.processInfo.isLowPowerModeEnabled {
    reduceRefreshFrequency()
}
NotificationCenter.default.addObserver(forName: .NSProcessInfoPowerStateDidChange,
                                       object: nil, queue: .main) { _ in updatePolicy() }

Also check ProcessInfo.processInfo.thermalState and do less when the device is hot.

How to measure

  • Xcode Debug navigator > Energy Impact gauge while running.
  • Instruments > Power Profiler (Xcode 26): power per CPU, GPU, display and network.
  • MetricKit and Xcode Organizer > Energy: field data, including background CPU use and energy reports.

Common follow-up questions

  • Why is polling bad? It wakes the CPU and radio again and again, even when nothing changed.
  • Does dark mode really save energy? On OLED displays, yes, because black pixels are off.
  • What is "tail energy"? The radio stays in a high-power state for a few seconds after a transfer. Many small requests pay it many times.
  • What happens if the app uses too much CPU in the background? The system can kill it, and the crash report shows a CPU resource exception.