Skip to content
All essays
iOSMarch 27, 202514 min

Swift Performance Optimization: Best Practices

Optimize your Swift code for better performance. Memory management, algorithms, and profiling techniques.

Ü
Ümit Uz
Mobile & Full Stack Developer

Writing performant Swift code is crucial for delivering smooth user experiences. Learn techniques to optimize your iOS applications.

Value Types vs Reference Types

swift
// Struct (Value Type) - Preferred for small data
struct User {
    let id: String
    let name: String
    var age: Int
}

// Class (Reference Type) - Use for identity or large data
class DatabaseConnection {
    private let connectionString: String

    init(connectionString: String) {
        self.connectionString = connectionString
    }
}

// Copy-on-Write optimization
let user1 = User(id: "1", name: "John", age: 25)
var user2 = user1 // No copy yet
user2.age = 26 // Copy happens here

Memory Management

Avoid Retain Cycles

swift
// BAD: Retain cycle
class ViewController {
    var closure: (() -> Void)?

    func setup() {
        closure = {
            self.doSomething()
        }
    }
}

// GOOD: Use [weak self]
class ViewController {
    var closure: (() -> Void)?

    func setup() {
        closure = { [weak self] in
            self?.doSomething()
        }
    }
}

Lazy Loading

swift
class DataProvider {
    // Lazy loaded property
    lazy var expensiveData: [Int] = {
        print("Computing expensive data...")
        return Array(1...1000000).map { $0 * 2 }
    }()

    // Lazy with computed property
    private var _cachedValue: String?

    var cachedValue: String {
        if let cached = _cachedValue {
            return cached
        }
        let value = computeExpensiveValue()
        _cachedValue = value
        return value
    }
}

Collection Optimization

Array Performance

swift
// Pre-allocate capacity
var items: [Int] = []
items.reserveCapacity(1000) // Avoids reallocation

// Use appropriate data structures
// Array: Random access, ordered
let array = [1, 2, 3, 4, 5]
let first = array[0] // O(1)

// Set: Fast lookup, unordered
let set: Set<Int> = [1, 2, 3, 4, 5]
let contains = set.contains(3) // O(1)

// Dictionary: Key-value pairs
let dict: [String: Int] = ["one": 1, "two": 2]
let value = dict["two"] // O(1)

String Operations

swift
// BAD: String concatenation in loop
var result = ""
for i in 0..<1000 {
    result += "\(i)" // Creates new string each time
}

// GOOD: Use StringBuilder pattern
var result = ""
let parts: [String] = (0..<1000).map { "\($0)" }
result = parts.joined()

// BETTER: Use reserveCapacity
var result = ""
result.reserveCapacity(5000)
for i in 0..<1000 {
    result.append("\(i)")
}

Algorithmic Optimization

Time Complexity

swift
// O(n²) - Nested loops
func findDuplicates(_ array: [Int]) -> [Int] {
    var duplicates: [Int] = []
    for i in 0..<array.count {
        for j in (i+1)..<array.count {
            if array[i] == array[j] {
                duplicates.append(array[i])
            }
        }
    }
    return duplicates
}

// O(n) - Using Set
func findDuplicatesFast(_ array: [Int]) -> [Int] {
    var seen = Set<Int>()
    var duplicates: [Int] = []
    for item in array {
        if seen.contains(item) {
            duplicates.append(item)
        } else {
            seen.insert(item)
        }
    }
    return duplicates
}

Concurrency for Performance

swift
class ImageProcessor {
    // Process images concurrently
    func processImages(_ urls: [URL]) async throws -> [Image] {
        try await withThrowingTaskGroup(of: (Int, Image).self) { group in
            for (index, url) in urls.enumerated() {
                group.addTask {
                    let image = try await self.downloadAndProcess(url)
                    return (index, image)
                }
            }

            // Collect results
            var results: [(Int, Image)] = []
            for try await (index, image) in group {
                results.append((index, image))
            }

            // Sort by original order
            return results.sorted { $0.0 < $1.0 }.map { $0.1 }
        }
    }

    private func downloadAndProcess(_ url: URL) async throws -> Image {
        // Download and process image
        return Image()
    }
}

Profiling Tools

Instruments

bash
# Launch Instruments
# Product > Profile > Time Profiler

# Check for:
# - High CPU usage
# - Memory leaks
# - Retain cycles
# - Slow operations

Metrics in Code

swift
import os.signpost

let log = OSLog(subsystem: "com.app.myapp", category: "Performance")

func processLargeData() {
    os_signpost(.begin, log: log, name: "Data Processing")
    defer { os_signpost(.end, log: log, name: "Data Processing") }

    // Processing code
}

SwiftUI Performance

Lazy Loading

swift
// BAD: Loads all items at once
List {
    ForEach(items) { item in
        ItemView(item: item)
    }
}

// GOOD: Lazy loads items
List {
    ForEach(items) { item in
        ItemView(item: item)
    }
}

View Optimization

swift
struct OptimizedView: View {
    @State private var items: [Item] = []

    var body: some View {
        List {
            ForEach(items) { item in
                ItemCell(item: item)
            }
        }
        .onAppear {
            loadItems()
        }
    }

    private func loadItems() {
        // Load in batches
        Task {
            let initial = await fetchItems(count: 20)
            items = initial
        }
    }
}

// Extract to separate view for better performance
struct ItemCell: View {
    let item: Item

    var body: some View {
        HStack {
            Text(item.title)
            Text(item.description)
        }
    }
}

Caching Strategies

swift
actor CacheManager {
    private var storage: [String: Any] = [:]

    func get<T>(_ key: String) -> T? {
        storage[key] as? T
    }

    func set<T>(_ value: T, forKey key: String) {
        storage[key] = value
    }

    func clear() {
        storage.removeAll()
    }
}

// Usage
let cache = CacheManager()

func getCachedData(for key: String) async -> Data? {
    if let cached = await cache.get(key) as? Data {
        return cached
    }

    let data = await fetchData(key)
    await cache.set(data, forKey: key)
    return data
}

Best Practices

  1. 1Profile first: Measure before optimizing
  2. 2Use value types: Prefer structs for small data
  3. 3Avoid force unwraps: Use safe optional handling
  4. 4Lazy loading: Load data only when needed
  5. 5Caching: Cache expensive computations
  6. 6Concurrency: Use async/await for I/O operations
  7. 7Memory: Be mindful of retain cycles
  8. 8Algorithms: Choose appropriate data structures

Performance optimization is an ongoing process. Always measure the impact of your optimizations!

Related essays

Next essay
SwiftData Complete Guide: Modern Data Persistence