iOS Performance · v1.0.0 · 2026-03-29 · sha256 6cc69d20bd46ca15

iOS Performance v1.0.0A

Immutable. This exact content is served forever at /api/v1/blob/6cc69d20bd46ca15.

---
name: iOS Performance
description: Memory management, profiling, optimization techniques, and Instruments usage for high-performance iOS apps
version: 1.0.0
---

# iOS Performance Optimization

Comprehensive guide to building high-performance iOS applications with proper memory management, profiling, and optimization techniques.

## Memory Management

### ARC (Automatic Reference Counting)

#### Strong vs Weak vs Unowned

```swift
// STRONG (default) - Increases retain count
class Parent {
    var child: Child?  // Strong reference
}

// WEAK - Does NOT increase retain count, becomes nil when deallocated
class Child {
    weak var parent: Parent?  // Weak to prevent retain cycle
}

// UNOWNED - Does NOT increase retain count, crashes if accessed after deallocation
class Customer {
    let card: CreditCard  // Customer owns the card

    init() {
        card = CreditCard(holder: self)
    }
}

class CreditCard {
    unowned let holder: Customer  // Card always has a holder
}
```

#### When to Use Each

| Reference Type | Use When | Risk |
|---------------|----------|------|
| `strong` | Object should stay alive | Retain cycles |
| `weak` | Optional reference, may become nil | None |
| `unowned` | Non-optional, same or longer lifetime | Crash if wrong |

### Retain Cycle Detection

#### Common Retain Cycle Patterns

```swift
// ❌ RETAIN CYCLE - Closure captures self strongly
class ViewController: UIViewController {
    var onComplete: (() -> Void)?

    func setup() {
        onComplete = {
            self.doSomething()  // Strong capture!
        }
    }
}

// ✅ FIXED - Weak capture
class ViewController: UIViewController {
    var onComplete: (() -> Void)?

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

#### Delegate Retain Cycles

```swift
// ❌ RETAIN CYCLE
protocol MyDelegate {
    func didComplete()
}

class Parent: MyDelegate {
    var child: Child?

    init() {
        child = Child()
        child?.delegate = self  // Strong reference back!
    }
}

class Child {
    var delegate: MyDelegate?  // Strong!
}

// ✅ FIXED - Weak delegate
protocol MyDelegate: AnyObject {  // Must be AnyObject for weak
    func didComplete()
}

class Child {
    weak var delegate: MyDelegate?  // Weak!
}
```

#### Timer Retain Cycles

```swift
// ❌ RETAIN CYCLE - Timer retains target
class ViewController: UIViewController {
    var timer: Timer?

    override func viewDidLoad() {
        timer = Timer.scheduledTimer(
            timeInterval: 1.0,
            target: self,        // Timer retains self!
            selector: #selector(tick),
            userInfo: nil,
            repeats: true
        )
    }
}

// ✅ FIXED - Use closure-based timer with weak capture
class ViewController: UIViewController {
    var timer: Timer?

    override func viewDidLoad() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            self?.tick()
        }
    }

    deinit {
        timer?.invalidate()
    }
}
```

### Memory Leak Checklist

```swift
// Add to your ViewControllers to verify deallocation
deinit {
    print("✅ \(Self.self) deallocated")
}
```

**Common Leak Sources:**
- [ ] Closures without `[weak self]`
- [ ] Delegates not marked `weak`
- [ ] NotificationCenter observers not removed
- [ ] Timers not invalidated
- [ ] DispatchWorkItem not cancelled
- [ ] Strong references in singletons

## Performance Profiling

### Instruments Usage

#### Time Profiler

```bash
# Launch Time Profiler
xcrun xctrace record --template "Time Profiler" --launch -- /path/to/app.app
```

**What to look for:**
- Functions taking > 16ms (60fps = 16.67ms per frame)
- Main thread blocking
- Excessive CPU usage

#### Allocations

```bash
# Track memory allocations
xcrun xctrace record --template "Allocations" --launch -- /path/to/app.app
```

**What to look for:**
- Memory growth over time (leaks)
- Transient allocations (excessive object creation)
- Large allocations

#### Leaks

```bash
# Detect memory leaks
xcrun xctrace record --template "Leaks" --launch -- /path/to/app.app
```

### In-Code Profiling

```swift
// Measure execution time
func measureTime(_ label: String, block: () -> Void) {
    let start = CFAbsoluteTimeGetCurrent()
    block()
    let end = CFAbsoluteTimeGetCurrent()
    print("⏱ \(label): \((end - start) * 1000)ms")
}

// Usage
measureTime("Load Data") {
    loadData()
}

// Using os_signpost for Instruments
import os.signpost

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

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

    // ... work
}
```

## UIKit Optimization

### UITableView/UICollectionView

```swift
// ✅ OPTIMIZED TableView
final class OptimizedTableViewController: UIViewController {

    private let tableView: UITableView = {
        let tv = UITableView()
        tv.rowHeight = UITableView.automaticDimension
        tv.estimatedRowHeight = 60  // Provide estimate!
        return tv
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        // Register cells for reuse
        tableView.register(MyCell.self, forCellReuseIdentifier: "MyCell")

        // Prefetching for smooth scrolling
        tableView.prefetchDataSource = self
    }
}

extension OptimizedTableViewController: UITableViewDataSourcePrefetching {
    func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
        // Preload images or data for upcoming cells
        for indexPath in indexPaths {
            imageLoader.prefetch(url: items[indexPath.row].imageURL)
        }
    }

    func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
        // Cancel preloading for cells no longer needed
        for indexPath in indexPaths {
            imageLoader.cancel(url: items[indexPath.row].imageURL)
        }
    }
}
```

#### Cell Optimization

```swift
final class OptimizedCell: UITableViewCell {

    // Use layer properties for shadows (GPU accelerated)
    override func awakeFromNib() {
        super.awakeFromNib()

        // Rasterize complex views
        layer.shouldRasterize = true
        layer.rasterizationScale = UIScreen.main.scale

        // Opaque views are faster
        backgroundColor = .white
        isOpaque = true
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        // Cancel any pending image loads
        imageView?.image = nil
        imageLoadTask?.cancel()
    }
}
```

### Image Optimization

```swift
// ✅ Downscale images to display size
extension UIImage {
    func downscaled(to size: CGSize) -> UIImage {
        let renderer = UIGraphicsImageRenderer(size: size)
        return renderer.image { _ in
            draw(in: CGRect(origin: .zero, size: size))
        }
    }
}

// ✅ Load images asynchronously
final class AsyncImageLoader {
    private let cache = NSCache<NSString, UIImage>()
    private var tasks: [URL: URLSessionTask] = [:]

    func load(url: URL, into imageView: UIImageView) {
        // Check cache first
        if let cached = cache.object(forKey: url.absoluteString as NSString) {
            imageView.image = cached
            return
        }

        // Load asynchronously
        let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
            guard let data = data, let image = UIImage(data: data) else { return }

            // Cache the image
            self?.cache.setObject(image, forKey: url.absoluteString as NSString)

            DispatchQueue.main.async {
                imageView.image = image
            }
        }
        tasks[url] = task
        task.resume()
    }

    func cancel(url: URL) {
        tasks[url]?.cancel()
        tasks[url] = nil
    }
}
```

### View Hierarchy Optimization

```swift
// ❌ SLOW - Deep view hierarchy
view.addSubview(container1)
container1.addSubview(container2)
container2.addSubview(container3)
container3.addSubview(label)

// ✅ FAST - Flat hierarchy
view.addSubview(label)

// ❌ SLOW - Transparent layers
view.backgroundColor = UIColor.black.withAlphaComponent(0.5)

// ✅ FAST - Opaque layers where possible
view.backgroundColor = .black
view.alpha = 0.5  // Or use solid color if acceptable
```

## Concurrency Performance

### Main Thread Protection

```swift
// ✅ Keep UI work on main thread
func updateUI(with data: Data) {
    DispatchQueue.main.async {
        self.label.text = String(data: data, encoding: .utf8)
    }
}

// ✅ Heavy work off main thread
func processData() {
    Task.detached(priority: .userInitiated) {
        let result = await self.heavyComputation()
        await MainActor.run {
            self.updateUI(with: result)
        }
    }
}
```

### Batch Operations

```swift
// ❌ SLOW - Individual updates
for item in items {
    tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}

// ✅ FAST - Batch updates
tableView.performBatchUpdates {
    let indexPaths = items.indices.map { IndexPath(row: $0, section: 0) }
    tableView.insertRows(at: indexPaths, with: .automatic)
}
```

## Network Performance

### Request Optimization

```swift
final class OptimizedNetworkClient {
    private let session: URLSession

    init() {
        let config = URLSessionConfiguration.default

        // Connection pooling
        config.httpMaximumConnectionsPerHost = 6

        // Caching
        config.urlCache = URLCache(
            memoryCapacity: 50_000_000,  // 50MB memory
            diskCapacity: 100_000_000     // 100MB disk
        )
        config.requestCachePolicy = .returnCacheDataElseLoad

        // Timeouts
        config.timeoutIntervalForRequest = 30
        config.timeoutIntervalForResource = 300

        session = URLSession(configuration: config)
    }

    func fetch<T: Decodable>(_ url: URL) async throws -> T {
        var request = URLRequest(url: url)

        // Enable compression
        request.setValue("gzip, deflate", forHTTPHeaderField: "Accept-Encoding")

        let (data, _) = try await session.data(for: request)
        return try JSONDecoder().decode(T.self, from: data)
    }
}
```

## Core Data Performance

```swift
// ✅ Batch fetching
let request = NSFetchRequest<Entity>(entityName: "Entity")
request.fetchBatchSize = 20  // Fetch in batches
request.returnsObjectsAsFaults = true  // Lazy loading

// ✅ Background context for heavy operations
let backgroundContext = persistentContainer.newBackgroundContext()
backgroundContext.perform {
    // Heavy operations here
    try? backgroundContext.save()
}

// ✅ Batch delete (much faster than individual deletes)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
try context.execute(deleteRequest)
```

## Performance Checklist

### Launch Time
- [ ] Defer non-essential work with `DispatchQueue.main.async`
- [ ] Lazy-load view controllers
- [ ] Use static/let for constants
- [ ] Minimize work in `application(_:didFinishLaunchingWithOptions:)`

### Scrolling
- [ ] Reuse cells properly
- [ ] Use estimated heights
- [ ] Prefetch data
- [ ] Avoid transparent views
- [ ] Cache computed values

### Memory
- [ ] Use `[weak self]` in closures
- [ ] Make delegates weak
- [ ] Invalidate timers
- [ ] Remove notification observers
- [ ] Profile with Instruments

### Network
- [ ] Enable HTTP caching
- [ ] Use compression
- [ ] Batch requests where possible
- [ ] Cancel unnecessary requests

## Quick Wins

| Issue | Solution |
|-------|----------|
| Slow scrolling | Enable cell reuse, prefetching |
| High memory | Downscale images, check leaks |
| Slow launch | Defer work, lazy load |
| UI freezes | Move work off main thread |
| Battery drain | Reduce background work, use efficient APIs |