.agents/skills/swift-concurrency/references/performance.md
Use this when:
Skip this file if:
actors.md or sendable.md.memory-management.md.Jump to:
Can't improve what you don't measure. Establish baseline before optimizing.
Synchronous → Asynchronous → Parallel
Move right only when proven necessary.
Too much work on main thread causes interface freezes.
Heavy work funneled into single task instead of parallel execution.
Tasks waiting on busy actor, causing unnecessary suspensions.
Profile with CMD + I → Select "Swift Concurrency" template.
Instruments included:
Tasks:
- Total count
- Running vs suspended
- Task states (Creating, Running, Suspended, Ending)
Actors:
- Queue size
- Execution time
- Contention points
Main Thread:
- Hangs
- Blocked time
Course Deep Dive: This topic is covered in detail in Lesson 10.1: Using Xcode Instruments to find performance bottlenecks
// ❌ All work on main thread
@MainActor
func generateWallpapers() {
Task {
for _ in 0..<100 {
let image = generator.generate() // Blocks main thread
wallpapers.append(image)
}
}
}
Instruments shows: Long main thread hang, no parallelism.
@MainActor
func generateWallpapers() {
Task {
for _ in 0..<100 {
let image = await backgroundGenerator.generate()
wallpapers.append(image)
}
}
}
actor BackgroundGenerator {
func generate() -> Image {
// Heavy work in background
}
}
actor Generator {
func generate() -> Image {
// Heavy work
}
}
// ❌ Sequential through actor
for _ in 0..<100 {
let image = await generator.generate() // Queue size = 1
}
Instruments shows: Actor queue never exceeds 1, no parallelism.
struct Generator {
@concurrent
static func generate() async -> Image {
// Heavy work, no shared state
}
}
// ✅ Parallel execution
for i in 0..<100 {
Task(name: "Image \(i)") {
let image = await Generator.generate()
await addToCollection(image)
}
}
Every await is potential suspension point:
let data = await fetchData() // May suspend
Not guaranteed - if isolation matches, may not suspend.
Code between suspension points. Larger = harder to reason about:
// ❌ Unnecessary async
private func scale(_ image: CGImage) async { }
func process(_ image: CGImage) async {
let scaled = await scale(image) // Suspension point
}
// ✅ Synchronous helper
private 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.
// ❌ Reenters actor
actor BankAccount {
func deposit(_ amount: Int) async {
balance += amount
await logTransaction() // Leaves actor
balance += bonus // Reenters - state may have changed
}
}
// ✅ Complete work before leaving
actor BankAccount {
func deposit(_ amount: Int) async {
balance += amount
balance += bonus
await logTransaction() // Leave after state changes
}
}
// ❌ Switches isolation
@MainActor
func update() async {
await process() // Switches away from main actor
}
// ✅ Inherits isolation (still requires await -- but no executor hop)
@MainActor
func update() async {
await process() // Stays on main actor when nonisolated(nonsending)
}
nonisolated(nonsending) func process() async { }
Course Deep Dive: This topic is covered in detail in Lesson 10.2: Reducing suspension points by managing isolation effectively
// ❌ May suspend
try await Task.checkCancellation()
// ✅ No suspension
if Task.isCancelled {
return
}
// ❌ Sequential
for url in urls {
let image = await download(url)
images.append(image)
}
// ✅ Parallel
await withTaskGroup(of: Image.self) { group in
for url in urls {
group.addTask { await download(url) }
}
for await image in group {
images.append(image)
}
}
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)
}
}
// Before: Two suspensions
Task {
let data = await generate() // Suspension 1
self.items.append(data) // Suspension 2 (back to main)
}
> **Course Deep Dive**: This topic is covered in detail in [Lesson 10.3: Using Xcode Instruments to detect and remove suspension points](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
// After: One suspension
Task { @concurrent in
let data = generate() // No suspension (synchronous)
await MainActor.run {
self.items.append(data) // Suspension 1 (to main)
}
}
Use async/parallel if:
2+ checks → async/parallel justified.
// Start here
func processData(_ data: Data) -> Result {
// Fast, in-memory work
}
Only move to async if:
func processData(_ data: Data) async -> Result {
// Use when:
// - Touches persistent storage
// - Parses large datasets
// - Network communication
// - Proven slow by profiling
}
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
> **Course Deep Dive**: This topic is covered in detail in [Lesson 10.4: How to choose between serialized, asynchronous, and parallel execution](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
// - Work scales with collection size
// - Proven beneficial by profiling
Benefits:
Costs:
// ❌ Over-parallelization
for i in 0..<1000 {
Task { await lightWork(i) }
}
// Creates 1000 tasks for trivial work
Better: Batch work or use fewer tasks.
// 80ms on main thread, but animation stutters
@MainActor
func process() {
heavyWork() // Freezes UI for 1 frame
}
// 100ms total, but smooth UI
@MainActor
func process() async {
await backgroundWork() // UI stays responsive
}
Perception: Smooth feels faster than raw speed.
@MainActor
func 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.
Before optimizing, ask:
// Before
@MainActor
func generate() {
for _ in 0..<100 {
let item = heavyGeneration()
items.append(item)
}
}
// After
@MainActor
func generate() async {
for _ in 0..<100 {
let item = await backgroundGenerate()
items.append(item)
}
}
@concurrent
func backgroundGenerate() async -> Item {
// Heavy work off main thread
}
// Before: Sequential
for url in urls {
let image = await download(url)
images.append(image)
}
// After: Parallel
await withTaskGroup(of: Image.self) { group in
for url in urls {
group.addTask { await download(url) }
}
for await image in group {
images.append(image)
}
}
// Before: Multiple hops
actor 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 fetches
actor 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
}
}
For real-world optimization examples, profiling techniques, and advanced performance patterns, see Swift Concurrency Course.