Performance
Performance
Section titled “Performance”Use this when:
- Async code is slower than expected or causing UI hangs.
- You need to choose between synchronous, asynchronous, and parallel execution.
- You are profiling concurrency overhead with Instruments.
Skip this file if:
- The issue is a compiler diagnostic about isolation or Sendable. Use
actors.mdorsendable.md. - You mainly need to fix a memory leak. Use
memory-management.md.
Jump to:
- Core Principles
- Common Performance Issues
- Using Xcode Instruments
- Suspension Points / Reducing Suspensions
- Choosing Execution Style
- Parallelism Costs
- Optimization Checklist
Core Principles
Section titled “Core Principles”Measurement is essential
Section titled “Measurement is essential”Can’t improve what you don’t measure. Establish baseline before optimizing.
Start simple, optimize later
Section titled “Start simple, optimize later”Synchronous → Asynchronous → ParallelMove right only when proven necessary.
Three phases of concurrency
Section titled “Three phases of concurrency”- No concurrency - Synchronous method
- Suspend without parallelism - Asynchronous method
- Advanced concurrency - Parallel execution
Common Performance Issues
Section titled “Common Performance Issues”UI hangs
Section titled “UI hangs”Too much work on main thread causes interface freezes.
Poor parallelization
Section titled “Poor parallelization”Heavy work funneled into single task instead of parallel execution.
Actor contention
Section titled “Actor contention”Tasks waiting on busy actor, causing unnecessary suspensions.
Using Xcode Instruments
Section titled “Using Xcode Instruments”Swift Concurrency template
Section titled “Swift Concurrency template”Profile with CMD + I → Select “Swift Concurrency” template.
Instruments included:
- Swift Tasks: Track running, alive, total tasks
- Swift Actors: Show actor execution and queue size
Key metrics
Section titled “Key metrics”Tasks:- Total count- Running vs suspended- Task states (Creating, Running, Suspended, Ending)
Actors:- Queue size- Execution time- Contention points
Main Thread:- Hangs- Blocked timeTask states
Section titled “Task states”- Creating: Task being initialized
- Running: Actively executing
- Suspended: Waiting (at await)
- Ending: Completing
Identifying Issues
Section titled “Identifying Issues”Main thread blocked
Section titled “Main thread blocked”// ❌ All work on main thread@MainActorfunc generateWallpapers() { Task { for _ in 0..<100 { let image = generator.generate() // Blocks main thread wallpapers.append(image) } }}Instruments shows: Long main thread hang, no parallelism.
Solution: Move to background
Section titled “Solution: Move to background”@MainActorfunc generateWallpapers() { Task { for _ in 0..<100 { let image = await backgroundGenerator.generate() wallpapers.append(image) } }}
actor BackgroundGenerator { func generate() -> Image { // Heavy work in background }}Actor contention
Section titled “Actor contention”actor Generator { func generate() -> Image { // Heavy work }}
// ❌ Sequential through actorfor _ in 0..<100 { let image = await generator.generate() // Queue size = 1}Instruments shows: Actor queue never exceeds 1, no parallelism.
Solution: Remove unnecessary actor
Section titled “Solution: Remove unnecessary actor”struct Generator { @concurrent static func generate() async -> Image { // Heavy work, no shared state }}
// ✅ Parallel executionfor i in 0..<100 { Task(name: "Image \(i)") { let image = await Generator.generate() await addToCollection(image) }}Suspension Points
Section titled “Suspension Points”What creates suspension
Section titled “What creates suspension”Every await is potential suspension point:
let data = await fetchData() // May suspendNot guaranteed - if isolation matches, may not suspend.
Suspension surface area
Section titled “Suspension surface area”Code between suspension points. Larger = harder to reason about:
- Actor invariants
- Performance
- Thread hops
- Reentrancy
- State consistency
- Do work before crossing isolation
- Cross once
- Finish job
- Only cross again when necessary
Reducing Suspensions
Section titled “Reducing Suspensions”1. Use synchronous methods
Section titled “1. Use synchronous methods”// ❌ Unnecessary asyncprivate func scale(_ image: CGImage) async { }
func process(_ image: CGImage) async { let scaled = await scale(image) // Suspension point}
// ✅ Synchronous helperprivate func scale(_ image: CGImage) { }
func process(_ image: CGImage) async { let scaled = scale(image) // No suspension}Rule: If method doesn’t need to suspend, don’t mark async.
2. Prevent actor reentrancy
Section titled “2. Prevent actor reentrancy”// ❌ Reenters actoractor BankAccount { func deposit(_ amount: Int) async { balance += amount await logTransaction() // Leaves actor balance += bonus // Reenters - state may have changed }}
// ✅ Complete work before leavingactor BankAccount { func deposit(_ amount: Int) async { balance += amount balance += bonus await logTransaction() // Leave after state changes }}3. Inherit isolation
Section titled “3. Inherit isolation”// ❌ Switches isolation@MainActorfunc update() async { await process() // Switches away from main actor}
// ✅ Inherits isolation (still requires await -- but no executor hop)@MainActorfunc update() async { await process() // Stays on main actor when nonisolated(nonsending)}
nonisolated(nonsending) func process() async { }4. Use non-suspending APIs
Section titled “4. Use non-suspending APIs”// ❌ May suspendtry await Task.checkCancellation()
// ✅ No suspensionif Task.isCancelled { return}5. Keep delay work off the main actor
Section titled “5. Keep delay work off the main actor”If the task only needs the main actor for the final mutation, do not start the whole retry flow on @MainActor.
// ❌ Can wait for MainActor, then suspend immediatelyregistrationRetryTask = Task { @MainActor [weak self] in try? await Task.sleep(for: .milliseconds(100)) guard let self else { return } self.registrationRetryTask = nil self.updateConnectedTargetWindow()}The delay itself is not UI work. Starting on @MainActor can add an avoidable executor wait before the task reaches Task.sleep, especially when the task is scheduled from another executor or while the main actor is busy.
// ✅ Sleep off-main, hop back only for the UI-owned workregistrationRetryTask = Task { @concurrent [weak self] in do { try await Task.sleep(for: .milliseconds(100)) } catch is CancellationError { return } guard let self else { return }
await MainActor.run { self.registrationRetryTask = nil self.updateConnectedTargetWindow() }}Use this pattern for delayed retries, backoff, and timer-like work where only the final state change is UI-owned.
6. Embrace parallelism
Section titled “6. Embrace parallelism”// ❌ Sequentialfor url in urls { let image = await download(url) images.append(image)}
// ✅ Parallelawait withTaskGroup(of: Image.self) { group in for url in urls { group.addTask { await download(url) } } for await image in group { images.append(image) }}Analyzing Suspensions in Instruments
Section titled “Analyzing Suspensions in Instruments”View task states
Section titled “View task states”- Select Swift Tasks instrument
- Switch to “Task States” view
- Look for Suspended states
- Check suspension duration
Navigate to code
Section titled “Navigate to code”- Click task state (Running/Suspended)
- Open Extended Detail
- Click related method
- Use “Open in Source Viewer”
Predict suspensions
Section titled “Predict suspensions”Task { // State 1: Running // State 2: Suspended (switch to background) let data = await backgroundWork() // State 3: Running (in background) // State 4: Suspended (switch to main actor) // State 5: Running (on main actor) await MainActor.run { updateUI(data) }}Optimization example
Section titled “Optimization example”// Before: Two suspensionsTask { let data = await generate() // Suspension 1 self.items.append(data) // Suspension 2 (back to main)}
// After: One suspensionTask { @concurrent in let data = generate() // No suspension (synchronous) await MainActor.run { self.items.append(data) // Suspension 1 (to main) }}Choosing Execution Style
Section titled “Choosing Execution Style”Decision checklist
Section titled “Decision checklist”Use async/parallel if:
- Blocks main actor visibly (>16ms)
- Scales with data (N items → N cost)
- Involves I/O (network, disk)
- Benefits from combining operations
- Called frequently
2+ checks → async/parallel justified.
Start synchronous
Section titled “Start synchronous”// Start herefunc processData(_ data: Data) -> Result { // Fast, in-memory work}Only move to async if:
- Instruments show main thread hang
- User reports sluggishness
- Work scales with input size
When to use async
Section titled “When to use async”func processData(_ data: Data) async -> Result { // Use when: // - Touches persistent storage // - Parses large datasets // - Network communication // - Proven slow by profiling}When to use parallel
Section titled “When to use parallel”await withTaskGroup(of: Result.self) { group in for item in items { group.addTask { await process(item) } }}
// Use when:// - Multiple independent operations// - Time-to-first-result matters
// - Work scales with collection size// - Proven beneficial by profilingParallelism Costs
Section titled “Parallelism Costs”Tradeoffs
Section titled “Tradeoffs”Benefits:
- Faster completion (if CPU-bound)
- Better resource utilization
- Improved responsiveness
Costs:
- Increased memory pressure
- CPU scheduling overhead
- System resource saturation
- Battery drain
- Thermal impact
When parallelism hurts
Section titled “When parallelism hurts”// ❌ Over-parallelizationfor i in 0..<1000 { Task { await lightWork(i) }}// Creates 1000 tasks for trivial workBetter: Batch work or use fewer tasks.
UX-Driven Decisions
Section titled “UX-Driven Decisions”Smooth animations > raw speed
Section titled “Smooth animations > raw speed”// 80ms on main thread, but animation stutters@MainActorfunc process() { heavyWork() // Freezes UI for 1 frame}
// 100ms total, but smooth UI@MainActorfunc process() async { await backgroundWork() // UI stays responsive}Perception: Smooth feels faster than raw speed.
Progress indication
Section titled “Progress indication”@MainActorfunc loadItems() async { isLoading = true
for i in 0..<100 { let item = await fetchItem(i) items.append(item) progress = Double(i) / 100 // Incremental updates }
isLoading = false}Background work + progress = feels faster.
Optimization Checklist
Section titled “Optimization Checklist”Before optimizing, ask:
- Have I profiled with Instruments?
- Is main thread actually blocked?
- Can this be synchronous?
- Am I over-parallelizing?
- Is actor contention the issue?
- Are suspensions necessary?
- Does UX require background work?
- Will this scale with data?
Common Patterns
Section titled “Common Patterns”Move heavy work to background
Section titled “Move heavy work to background”// Before@MainActorfunc generate() { for _ in 0..<100 { let item = heavyGeneration() items.append(item) }}
// After@MainActorfunc generate() async { for _ in 0..<100 { let item = await backgroundGenerate() items.append(item) }}
@concurrentfunc backgroundGenerate() async -> Item { // Heavy work off main thread}Parallelize independent work
Section titled “Parallelize independent work”// Before: Sequentialfor url in urls { let image = await download(url) images.append(image)}
// After: Parallelawait withTaskGroup(of: Image.self) { group in for url in urls { group.addTask { await download(url) } } for await image in group { images.append(image) }}Reduce actor hops
Section titled “Reduce actor hops”// Before: Multiple hopsactor Store { func process() async { let a = await fetch1() // Hop 1 let b = await fetch2() // Hop 2 let c = await fetch3() // Hop 3 combine(a, b, c) }}
// After: Batch fetchesactor Store { func process() async { async let a = fetch1() async let b = fetch2() async let c = fetch3() combine(await a, await b, await c) // One hop }}Best Practices
Section titled “Best Practices”- Profile before optimizing - measure baseline
- Start synchronous - add async only when needed
- Use Instruments regularly - catch issues early
- Name tasks - easier debugging in Instruments
- Check suspension count - reduce unnecessary awaits
- Avoid premature parallelism - has costs
- Consider UX - smooth > fast
- Batch actor work - reduce contention
- Test on real devices - simulators lie
- Monitor in production - real usage patterns differ
Debugging Performance
Section titled “Debugging Performance”Instruments workflow
Section titled “Instruments workflow”- Profile with Swift Concurrency template
- Identify main thread hangs
- Check task parallelism
- Analyze actor queue sizes
- Review suspension points
- Navigate to problematic code
- Apply optimizations
- Re-profile to verify
Red flags in Instruments
Section titled “Red flags in Instruments”- Main thread blocked >16ms
- Actor queue size always 1
- High suspension count
- Tasks created but not running
- Excessive task creation (1000+)