| name | ios-performance |
| description | Diagnose and fix iOS app performance — slow launch, jank, hangs, memory leaks, battery drain, slow Xcode builds, large app size. Covers Instruments/xctrace, Memory Graph Debugger, MetricKit, XCTest perf gates, Swift 6.2 perf (ARC, COW, InlineArray, Span), SwiftUI render perf (@Observable, LazyVStack), ProMotion frame pacing, app-launch phases, build settings, binary-size reduction. Use when an app feels slow, freezes, hangs, leaks memory, drains battery, drops frames or is stuck at 60 Hz, launches or compiles slowly, or downloads too large. Trigger phrases: "performance", "memory leak", "retain cycle", "Time Profiler", "FPS", "ProMotion", "launch time", "app size", "build time", "optimize". |
| license | MIT |
iOS Performance
Measure first, optimize second. Guessing where a bottleneck is wastes more
time than profiling it. Every section below tells you which tool produces the
evidence, then how to act on that evidence.
Authored against iOS 26 / Swift 6.2 / Xcode 26. Performance numbers quoted
here are starting expectations — always re-measure on your oldest supported
device with a Release build.
What this skill covers (and where to go)
Performance is six distinct problems. Route by symptom:
Two golden rules apply to all of them:
- Profile on a real device with a Release build. The Simulator's CPU,
memory, and energy characteristics are not your users'. Debug builds carry
optimization-off overhead that distorts every number.
- Reproduce the specific slow operation, not the whole app. "Scrolling is
slow" → profile only scrolling. Isolate before you measure.
Related skills in this collection: ios-debugging (LLDB, crash symbolication,
Memory Graph deep-dives), swift-concurrency (correctness-focused actor/async
patterns), swiftui-ui (general SwiftUI), ios-testing (XCTest setup).
1. Profiling workflow (the evidence step)
Before opening Instruments, narrow what you're investigating, then pick the
template. See references/instruments-tools.md for the per-instrument deep
dives (Time Profiler call-tree reading, Allocations, Core Data, System Trace,
OSSignposter, the CLI tools).
App performance problem?
├─ Feels slow / laggy (UI stalls, scroll stutters) → Time Profiler (CPU)
├─ Memory grows over time → Allocations (object growth)
├─ Specific memory leak / retain cycle → Memory Graph Debugger
├─ UI frozen, unresponsive >1 s → Time Profiler + System Trace
├─ Slow data loading (Core Data / SQLite) → Core Data instrument / EXPLAIN
└─ Battery drains, device hot → Energy / Power Profiler
Time Profiler — the #1 mistake
Time Profiler shows CPU time per function. The mistake that wastes hours:
blaming the function with high Total Time.
- Self Time = time spent in that function's own code.
- Total Time = Self Time + everything it calls.
A function with 80% Total Time but 5% Self Time is calling slow code — drill
down to find the leaf with high Self Time. Always set Invert Call Tree,
Separate by Thread, and Hide System Libraries so the heaviest leaf
functions surface first and you can isolate main-thread work.
Quick patterns — full code in instruments-tools.md:
- CLI quick checks before a full session:
xcrun sample, xcrun leaks,
xcrun xctrace record.
OSSignposter measures specific operations you define (Points of
Interest instrument; bridges to automated regression tests).
- Regression-proof every fix — three stages: OSSignposter (dev) → XCTest
measure with a baseline (CI — a baseline-less measure {} test always
passes, it's theater) → MetricKit (production;
references/metrickit.md). XCTHitchMetric and XCTOSSignpostMetric cover
hitches and your own signposts.
2. Memory & leaks
90% of memory leaks follow 3 patterns: un-invalidated timers, un-removed
observers, and closures captured strongly in collections. Diagnose
systematically — never guess. Full pattern catalogue (6 leak patterns with
fixes, PhotoKit/AVFoundation request cancellation, the CLI memory toolkit) is in
references/memory-leaks.md.
Leak vs normal growth
Climbs-then-plateaus (or drops under pressure) = cache, normal; linear unbounded
growth on a repeated operation = leak. Verify with Xcode → Debug → Simulate
Memory Warning — full tell-tales in memory-leaks.md.
Mandatory first steps (before reading any code)
- Memory Graph Debugger (Debug → Memory Graph, or toolbar icon): look for
purple/red
⚠ circles → click → read the retain-cycle chain. Fastest path.
deinit logging on suspect classes — the cheapest leak detector:
final class DetailViewModel { deinit { print("✅ DetailViewModel deallocated") } }
Navigate in, navigate away. No "✅"? It's retained somewhere.
- Instruments → Leaks / Allocations: repeat the action 10×. Flat line = not
a leak (stop). Steady climb = leak (continue).
Core rules — full code + rationale in memory-leaks.md:
- Timers (the #1 leak):
[weak self] alone does not fix a timer leak —
the RunLoop retains a scheduled Timer; you must invalidate() it (or use
Combine's Timer.publish().autoconnect(), whose cancellable auto-cleans).
weak vs unowned: weak by default in escaping closures/delegates;
unowned (~2× faster) only when lifetime is guaranteed. Non-escaping closures
(map/filter/forEach…) need neither — [weak self] there is noise.
- Jetsam is not a crash: memory growing while in use = real leak; killed
while in the background = Jetsam — reduce background footprint (<50 MB) and
restore state so the user never notices.
3. Hangs (main thread blocked)
A hang is the main runloop failing to process events for >1 s — the user
taps and nothing happens. Distinct from a hitch (1–3 dropped frames during
animation) and general lag (sluggish but responsive). Every hang has exactly
one of two causes:
- Main thread BUSY — doing work it shouldn't (proactive computation,
unfiltered notification handling, a blocking API where an async one exists).
→ Time Profiler shows your code with high Self Time on the main thread.
- Main thread BLOCKED — waiting on something (sync file I/O, sync network,
a lock held by a background thread,
dispatch_sync/semaphore).
→ System Trace shows the main thread in a red (blocked) state.
Tool selection: reproducible → Time Profiler first (BUSY) then System Trace if
CPU is low but the thread is stalled (BLOCKED). Field-only → Xcode Organizer →
Hangs, or adopt MetricKit (MXHangDiagnostic).
The most common fixes (full code in hangs.md): sync file
I/O off-main via Task.detached / @concurrent (a plain Task {} inherits
@MainActor and stays on main); cache expensive formatters; never
semaphore-block to fake sync; replace main-thread lock contention with an actor;
filter notification observers by object:; never DispatchQueue.main.sync from
a background thread.
Also in hangs.md: watchdog terminations (the watchdog
kills apps that hang during launch ~20 s / background ~5 s / foreground ~10 s —
EXC_CRASH (SIGKILL), 0xDEAD10CC = held a file/SQLite lock while suspending;
disabled in Simulator and under the debugger), the full hang decision tree, and
the pre-ship hang prevention checklist.
4. App launch
Apple's target: first frame within ~400 ms, interactive by the time the
launch animation finishes. iOS runs a watchdog that kills apps overrunning the
budget. Launch has three phases — profile to learn which one dominates before
changing anything. Full phase model, measurement hygiene, push-notification
launch path, and the per-phase fix lists: references/app-launch.md.
Quick reference in app-launch.md: the launch
vocabulary (cold / warm / hot — a resume is NOT a launch), the three launch
phases (pre-main → first frame → interactive), DYLD_PRINT_STATISTICS=1
deadline triage, the XCTApplicationLaunchMetric regression test, and
measurement hygiene (Release build, oldest device; profiling ≠ measuring).
5. Energy & battery
Battery drain spans subsystems — diagnose the actual cause, don't assume it's
the network. Profile with the Energy Log / Power Profiler template; sustained
red/orange usage is the problem (brief peaks are fine). The 8 anti-patterns and
the WWDC-grade efficient-API reference (Timer/network/location coalescing,
BGContinuedProcessingTask, MetricKit energy metrics) are in
references/energy.md.
The recurring offenders (timers/polling, full-accuracy location, off-screen
animations, chatty network, non-compliant background work) and the per-symptom
diagnosis paths ("top of Battery Settings", "device gets hot", "background
drain") are in energy.md.
6. SwiftUI render performance
Slow scrolling and dropped frames in SwiftUI almost always trace to too much
body re-evaluation. The full reference (view identity, EquatableView, the
LazyVStack/List decision matrix, image downsampling, .task cancellation,
Self._printChanges(), the SwiftUI Instrument) is in references/swiftui-perf.md.
The four levers — full code and tables in swiftui-perf.md:
@Observable over ObservableObject (per-property tracking — the single
biggest SwiftUI perf win), extract subviews to localize invalidation,
lazy containers for long lists (VStack <50 / LazyVStack / List), and
stable ForEach identity (never an array index). Diagnose with
Self._printChanges() + the Xcode 16+ SwiftUI Instrument; downsample images at
decode time (a 4000×3000 photo decoded full-size costs ~48 MB even at 80×80 pt).
Core Image / camera pipelines: coreimage-camera-perf.md;
Metal render loop: metal-render-loop.md.
Display refresh, ProMotion & Metal frame timing
Stuck at 60 Hz on a ProMotion panel, animation jitter despite good average FPS,
or a Metal/Core Animation loop missing its GPU deadline are frame-budget and
frame-pacing problems, not SwiftUI-body problems — ProMotion enablement,
CADisplayLink/CAMetalDisplayLink, commit-vs-render hitches, frame pacing,
and the thermal/low-power caps are in display-promotion.md.
7. Swift language performance
When Time Profiler points at your Swift code in a hot path, the WWDC "four
principles" tell you which lever to pull: function-call overhead, memory
allocation, memory layout, value copying. The complete treatment (noncopyable
types, COW internals & defensive copies, ARC closure-capture costs, generic
specialization & existential containers, @inlinable, collection performance,
memory layout/padding, exclusivity checks, typed throws, full Span API) is in
references/swift-performance.md. The highest-leverage points:
Its "Highest-leverage points" section condenses them: value-vs-reference & COW
defensive copies, ARC/closure costs (non-escaping = zero ARC; unowned ~2×
faster than weak), existential vs specialized generics (any P ~10× slower
than some P), the Swift 6.2 tools (InlineArray, Span, @concurrent), and
the anti-pattern table.
When source-level reasoning can't explain a Time-Profiler hotspot, inspect
optimized SIL (swiftc -O -emit-sil) — see references/sil-inspection.md for
instruction-search targets and false-conclusion guardrails. For C++-interop hot
paths, custom sorted collections, and the Swift 6.2 strict-memory-safety audit
gate, see references/swift-performance.md (Parts 12–13) and
references/custom-collections.md.
8. Build-time performance
Slow Xcode builds are a productivity tax. Two fronts: build settings (often
misconfigured after migrating across Xcode versions) and source-level
compile-time hotspots (Swift type-checker blowups). Full settings reference,
source-fix before/after patterns, compile-time diagnostic flags, and SPM graph
analysis are in references/build-performance.md.
Its "Quick reference" section carries the essentials: measure the median of
repeated builds (standard-clean / cached-clean / incremental), the
highest-impact settings table (Debug singlefile+-Onone, Release
wholemodule, compilation caching on), the compile-time hotspot flags
(-warn-long-function-bodies=100) with the standard source fixes, and
SPM-over-CocoaPods guidance (CocoaPods is deprecated).
9. App-size / binary-size reduction
Large download size loses installs (and >200 MB requires Wi-Fi to download). Reduce
it tier by tier, re-measuring a clean Release archive after each pass and
reverting any pass that gains less than your threshold or fails a gate. The full
tiered catalogue (P1–P20 with per-technique risk/quality/perf trade-offs), the
baseline-capture procedure, the validation gates, and the JSON report schema are
in references/binary-size.md.
The guardrails (never -Ounchecked or -enforce-exclusivity=unchecked in
production, never drop dSYMs, one risk tier at a time), the Tier 1→3 summary
table, and the highest-ROI / highest-risk callouts are in
binary-size.md alongside the full catalogue.
Performance targets & deadline pressure
The concrete-thresholds table (cold launch <400 ms to first frame, 16.67 ms /
8.33 ms frame budgets, memory/CPU/app-size limits) and the cross-cutting "make
it faster, we ship tomorrow" playbook are at the end of
instruments-tools.md. The math always favors
profiling first: ~15–20 min to profile the critical path vs 2+ hours wasted
optimizing the wrong function.
Reference index
| File | Contents |
|---|
| references/instruments-tools.md | Time Profiler / Allocations / Core Data / System Trace deep dives, OSSignposter, CLI tools, LLDB-at-the-freeze-point |
| references/memory-leaks.md | 6 leak patterns + fixes, PhotoKit/AVFoundation cancellation, CLI memory toolkit, intermittent-leak field diagnostics |
| references/hangs.md | Full hang decision tree, 8 patterns, Organizer/MetricKit hang reports, watchdog detail |
| references/app-launch.md | Launch phase model, hygiene, per-phase fixes, push-notification launch, commands |
| references/energy.md | 8 energy anti-patterns, efficient-API reference, symptom routing, MetricKit energy metrics |
| references/swiftui-perf.md | View identity, EquatableView, list decision, image downsampling, .task, Self._printChanges() |
| references/display-promotion.md | ProMotion 120 Hz enablement, CADisplayLink/CAMetalDisplayLink, frame budgets, commit-vs-render hitches, frame pacing, GPU frame-time measurement, thermal/low-power caps, adaptive frame rate |
| references/swift-performance.md | Noncopyable, COW, ARC, generics/existentials (incl. protocol-extension static dispatch, any P usability), inlining, collections, memory layout, exclusivity, typed throws, full Span/InlineArray, C++ interop deep-copy, strict memory safety |
| references/sil-inspection.md | Optimized SIL as evidence — generation forms, grouped instruction-search targets, false-conclusion guardrails, before/after workflow |
| references/custom-collections.md | Implementing a sorted collection — structure selection, L1-tuned B-Tree order, unsafe-buffer nodes, COW traps |
| references/build-performance.md | Build-settings reference, zero-change/cached-clean methodology, critical-path triage, Debug post-processing & sanitizers, ccache, linker, Xcode IDE defaults, source fixes, SPM graph + cascades |
| references/binary-size.md | P1–P20 technique catalogue with trade-offs, baseline capture, validation gates, report schema |
| references/metal-render-loop.md | CPU↔GPU in-flight semaphore, Swift↔Metal uniform layout, setFragmentBytes, full-screen-triangle, ACES/PBR, MetalFX upscaling |
| references/coreimage-camera-perf.md | Long-lived CIContext, debounce live-edit re-encode, CGImageSource metadata reads, AVCaptureSession begin/commit |
| references/metrickit.md | MetricKit field-metric setup, payload parsing, cross-payload aggregation, ASC server-side diagnostics |