Files
gh-glittercowboy-taches-cc-…/skills/expertise/iphone-apps/references/performance.md
2025-11-29 18:28:37 +08:00

11 KiB

Performance

Instruments, memory management, launch optimization, and battery efficiency.

Instruments Profiling

Time Profiler

Find CPU-intensive code:

# Profile from CLI
xcrun xctrace record \
    --template 'Time Profiler' \
    --device-name 'iPhone 16' \
    --launch MyApp.app \
    --output profile.trace

Common issues:

  • Main thread work during UI updates
  • Expensive computations in body
  • Synchronous I/O

Allocations

Track memory usage:

xcrun xctrace record \
    --template 'Allocations' \
    --device-name 'iPhone 16' \
    --launch MyApp.app \
    --output allocations.trace

Look for:

  • Memory growth over time
  • Abandoned memory
  • High transient allocations

Leaks

Find retain cycles:

xcrun xctrace record \
    --template 'Leaks' \
    --device-name 'iPhone 16' \
    --launch MyApp.app \
    --output leaks.trace

Common causes:

  • Strong reference cycles in closures
  • Delegate patterns without weak references
  • Timer retain cycles

Memory Management

Weak References in Closures

// Bad - creates retain cycle
class ViewModel {
    var timer: Timer?

    func startTimer() {
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
            self.update()  // Strong capture
        }
    }
}

// Good - weak capture
class ViewModel {
    var timer: Timer?

    func startTimer() {
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            self?.update()
        }
    }

    deinit {
        timer?.invalidate()
    }
}

Async Task Cancellation

class ViewModel {
    private var loadTask: Task<Void, Never>?

    func load() {
        loadTask?.cancel()
        loadTask = Task { [weak self] in
            guard let self else { return }

            let items = try? await fetchItems()

            // Check cancellation before updating
            guard !Task.isCancelled else { return }

            await MainActor.run {
                self.items = items ?? []
            }
        }
    }

    deinit {
        loadTask?.cancel()
    }
}

Large Data Handling

// Bad - loads all into memory
let allPhotos = try await fetchAllPhotos()
for photo in allPhotos {
    process(photo)
}

// Good - stream processing
for await photo in fetchPhotosStream() {
    process(photo)

    // Allow UI updates
    if shouldYield {
        await Task.yield()
    }
}

SwiftUI Performance

Avoid Expensive Body Computations

// Bad - recomputes on every body call
struct ItemList: View {
    let items: [Item]

    var body: some View {
        let sortedItems = items.sorted { $0.date > $1.date }  // Every render!
        List(sortedItems) { item in
            ItemRow(item: item)
        }
    }
}

// Good - compute once
struct ItemList: View {
    let items: [Item]

    var sortedItems: [Item] {
        items.sorted { $0.date > $1.date }
    }

    var body: some View {
        List(sortedItems) { item in
            ItemRow(item: item)
        }
    }
}

// Better - use @State or computed in view model
struct ItemList: View {
    @State private var sortedItems: [Item] = []
    let items: [Item]

    var body: some View {
        List(sortedItems) { item in
            ItemRow(item: item)
        }
        .onChange(of: items) { _, newItems in
            sortedItems = newItems.sorted { $0.date > $1.date }
        }
    }
}

Optimize List Performance

// Use stable identifiers
struct Item: Identifiable {
    let id: UUID  // Stable identifier
    var name: String
}

// Explicit id for efficiency
List(items, id: \.id) { item in
    ItemRow(item: item)
}

// Lazy loading for large lists
LazyVStack {
    ForEach(items) { item in
        ItemRow(item: item)
    }
}

Equatable Conformance

// Prevent unnecessary re-renders
struct ItemRow: View, Equatable {
    let item: Item

    static func == (lhs: ItemRow, rhs: ItemRow) -> Bool {
        lhs.item.id == rhs.item.id &&
        lhs.item.name == rhs.item.name
    }

    var body: some View {
        Text(item.name)
    }
}

// Use in ForEach
ForEach(items) { item in
    ItemRow(item: item)
        .equatable()
}

Task Modifier Optimization

// Bad - recreates task on any state change
struct ContentView: View {
    @State private var items: [Item] = []
    @State private var searchText = ""

    var body: some View {
        List(filteredItems) { item in
            ItemRow(item: item)
        }
        .task {
            items = await fetchItems()  // Reruns when searchText changes!
        }
    }
}

// Good - use task(id:)
struct ContentView: View {
    @State private var items: [Item] = []
    @State private var searchText = ""
    @State private var needsLoad = true

    var body: some View {
        List(filteredItems) { item in
            ItemRow(item: item)
        }
        .task(id: needsLoad) {
            if needsLoad {
                items = await fetchItems()
                needsLoad = false
            }
        }
    }
}

Launch Time Optimization

Measure Launch Time

# Cold launch measurement
xcrun simctl spawn booted log stream --predicate 'subsystem == "com.apple.os.signpost" && category == "PointsOfInterest"'

In Instruments: App Launch template

Defer Non-Critical Work

@main
struct MyApp: App {
    init() {
        // Critical only
        setupErrorReporting()
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
                .task {
                    // Defer non-critical
                    await setupAnalytics()
                    await preloadData()
                }
        }
    }
}

Avoid Synchronous Work

// Bad - blocks launch
@main
struct MyApp: App {
    let database = Database.load()  // Synchronous I/O

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

// Good - async initialization
@main
struct MyApp: App {
    @State private var database: Database?

    var body: some Scene {
        WindowGroup {
            if let database {
                ContentView()
                    .environment(database)
            } else {
                LaunchScreen()
            }
        }
        .task {
            database = await Database.load()
        }
    }
}

Reduce Dylib Loading

  • Minimize third-party dependencies
  • Use static linking where possible
  • Merge frameworks

Network Performance

Request Batching

// Bad - many small requests
for id in itemIDs {
    let item = try await fetchItem(id)
    items.append(item)
}

// Good - batch request
let items = try await fetchItems(ids: itemIDs)

Image Loading

// Use AsyncImage with caching
AsyncImage(url: imageURL) { phase in
    switch phase {
    case .empty:
        ProgressView()
    case .success(let image):
        image.resizable().scaledToFit()
    case .failure:
        Image(systemName: "photo")
    @unknown default:
        EmptyView()
    }
}

// For better control, use custom caching
actor ImageCache {
    private var cache: [URL: UIImage] = [:]

    func image(for url: URL) async throws -> UIImage {
        if let cached = cache[url] {
            return cached
        }

        let (data, _) = try await URLSession.shared.data(from: url)
        let image = UIImage(data: data)!
        cache[url] = image
        return image
    }
}

Prefetching

struct ItemList: View {
    let items: [Item]
    let prefetcher = ImagePrefetcher()

    var body: some View {
        List(items) { item in
            ItemRow(item: item)
                .onAppear {
                    // Prefetch next items
                    let index = items.firstIndex(of: item) ?? 0
                    let nextItems = items.dropFirst(index + 1).prefix(5)
                    prefetcher.prefetch(urls: nextItems.compactMap(\.imageURL))
                }
        }
    }
}

Battery Optimization

Location Updates

import CoreLocation

class LocationService: NSObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()

    func startUpdates() {
        // Use appropriate accuracy
        manager.desiredAccuracy = kCLLocationAccuracyHundredMeters  // Not kCLLocationAccuracyBest

        // Allow deferred updates
        manager.allowsBackgroundLocationUpdates = false
        manager.pausesLocationUpdatesAutomatically = true

        // Use significant change for background
        manager.startMonitoringSignificantLocationChanges()
    }
}

Background Tasks

import BackgroundTasks

func scheduleAppRefresh() {
    let request = BGAppRefreshTaskRequest(identifier: "com.app.refresh")
    request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)  // 15 minutes

    do {
        try BGTaskScheduler.shared.submit(request)
    } catch {
        print("Could not schedule app refresh: \(error)")
    }
}

func handleAppRefresh(task: BGAppRefreshTask) {
    // Schedule next refresh
    scheduleAppRefresh()

    let refreshTask = Task {
        do {
            try await syncData()
            task.setTaskCompleted(success: true)
        } catch {
            task.setTaskCompleted(success: false)
        }
    }

    task.expirationHandler = {
        refreshTask.cancel()
    }
}

Network Efficiency

// Use background URL session for large transfers
let config = URLSessionConfiguration.background(withIdentifier: "com.app.background")
config.isDiscretionary = true  // System chooses optimal time
config.allowsCellularAccess = false  // WiFi only for large downloads

let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)

Debugging Performance

Signposts

import os

let signposter = OSSignposter()

func processItems() async {
    let signpostID = signposter.makeSignpostID()
    let state = signposter.beginInterval("Process Items", id: signpostID)

    for item in items {
        signposter.emitEvent("Processing", id: signpostID, "\(item.name)")
        await process(item)
    }

    signposter.endInterval("Process Items", state)
}

MetricKit

import MetricKit

class MetricsManager: NSObject, MXMetricManagerSubscriber {
    override init() {
        super.init()
        MXMetricManager.shared.add(self)
    }

    func didReceive(_ payloads: [MXMetricPayload]) {
        for payload in payloads {
            // Process CPU, memory, launch time metrics
            if let cpuMetrics = payload.cpuMetrics {
                print("CPU time: \(cpuMetrics.cumulativeCPUTime)")
            }
        }
    }

    func didReceive(_ payloads: [MXDiagnosticPayload]) {
        for payload in payloads {
            // Process crash and hang diagnostics
        }
    }
}

Performance Checklist

Launch

  • < 400ms to first frame
  • No synchronous I/O in init
  • Deferred non-critical setup

Memory

  • No leaks
  • Stable memory usage
  • No abandoned memory

UI

  • 60 fps scrolling
  • No main thread blocking
  • Efficient list rendering

Network

  • Request batching
  • Image caching
  • Proper timeout handling

Battery

  • Minimal background activity
  • Efficient location usage
  • Discretionary transfers