skills/kaakati/rails-enterprise-dev/performance-optimization

performance-optimization

SKILL.md

Performance Optimization — Expert Decisions

Expert decision frameworks for performance choices. Claude knows lazy loading and async basics — this skill provides judgment calls for when to optimize and which tool to use.


Decision Trees

Should You Optimize?

When should you invest in optimization?
├─ User-facing latency issue (visible stutter/delay)
│  └─ YES — Profile and fix
│     Measure first, optimize second
├─ Premature concern ("this might be slow")
│  └─ NO — Wait for evidence
│     Write clean code, profile later
├─ Battery drain complaints
│  └─ YES — Use Energy Diagnostics
│     Focus on background work, location, network
├─ Memory warnings / crashes
│  └─ YES — Use Allocations + Leaks
│     Find retain cycles, unbounded caches
└─ App store reviews mention slowness
   └─ YES — Profile real scenarios
      User perception matters

The trap: Optimizing based on assumptions. Always profile first. The bottleneck is rarely where you think.

Profiling Tool Selection

What are you measuring?
├─ Slow UI / frame drops
│  └─ Time Profiler + View Debugger
│     Find expensive work on main thread
├─ Memory growth / leaks
│  └─ Allocations + Leaks instruments
│     Track object lifetimes, find cycles
├─ Network performance
│  └─ Network instrument + Charles/Proxyman
│     Latency, payload size, request count
├─ Disk I/O issues
│  └─ File Activity instrument
│     Excessive reads/writes
├─ Battery drain
│  └─ Energy Log instrument
│     CPU wake, location, networking
└─ GPU / rendering
   └─ Core Animation instrument
      Offscreen rendering, overdraw

SwiftUI View Update Strategy

View is re-rendering too often?
├─ Caused by parent state changes
│  └─ Extract to separate view
│     Child doesn't depend on changing state
├─ Complex computed body
│  └─ Cache expensive computations
│     Use ViewModel or memoization
├─ List items all updating
│  └─ Check view identity
│     Use stable IDs, not indices
├─ Observable causing cascading updates
│  └─ Split into multiple @Published
│     Or use computed properties
└─ Animation causing constant redraws
   └─ Use drawingGroup() or limit scope
      Rasterize stable content

Memory Management Decision

How to fix memory issues?
├─ Steady growth during use
│  └─ Check caches and collections
│     Add eviction, use NSCache
├─ Growth tied to navigation
│  └─ Check retain cycles
│     weak self in closures, delegates
├─ Large spikes on specific screens
│  └─ Downsample images
│     Load at display size, not full resolution
├─ Memory not released after screen dismissal
│  └─ Debug object lifecycle
│     deinit not called = retain cycle
└─ Background memory pressure
   └─ Respond to didReceiveMemoryWarning
      Clear caches, release non-essential data

NEVER Do

View Identity

NEVER use indices as identifiers:

// ❌ Identity changes when array mutates
List(items.indices, id: \.self) { index in
    ItemRow(item: items[index])
}
// Insert at index 0 → all views recreated!

// ✅ Use stable identifiers
List(items) { item in
    ItemRow(item: item)
        .id(item.id)  // Stable across mutations
}

NEVER compute expensive values in body:

// ❌ Called on every render
var body: some View {
    let sortedItems = items.sorted { $0.date > $1.date }  // O(n log n) per render!
    let filtered = sortedItems.filter { $0.isActive }

    List(filtered) { item in
        ItemRow(item: item)
    }
}

// ✅ Compute in ViewModel or use computed property
@MainActor
class ViewModel: ObservableObject {
    @Published var items: [Item] = []

    var displayItems: [Item] {
        items.filter(\.isActive).sorted { $0.date > $1.date }
    }
}

State Management

NEVER use @StateObject for passed objects:

// ❌ Creates new instance on every parent update
struct ChildView: View {
    @StateObject var viewModel: ChildViewModel  // Wrong!

    var body: some View { ... }
}

// ✅ Use @ObservedObject for passed objects
struct ChildView: View {
    @ObservedObject var viewModel: ChildViewModel  // Parent owns it

    var body: some View { ... }
}

NEVER make everything @Published:

// ❌ Every property change triggers view updates
class ViewModel: ObservableObject {
    @Published var items: [Item] = []
    @Published var internalCache: [String: Data] = [:]  // UI doesn't need this!
    @Published var isProcessing = false  // Maybe internal only
}

// ✅ Only publish what UI observes
class ViewModel: ObservableObject {
    @Published var items: [Item] = []
    @Published var isLoading = false

    private var internalCache: [String: Data] = [:]  // Not @Published
    private var isProcessing = false  // Private state
}

Memory Leaks

NEVER capture self strongly in escaping closures:

// ❌ Retain cycle — never deallocates
class ViewModel {
    var timer: Timer?

    func start() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            self.tick()  // Strong capture!
        }
    }
}

// ✅ Weak capture + invalidation
class ViewModel {
    var timer: Timer?

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

    deinit {
        timer?.invalidate()
    }
}

NEVER forget to remove observers:

// ❌ Leaks observer and potentially self
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleNotification),
            name: .userLoggedIn,
            object: nil
        )
        // Never removed!
    }
}

// ✅ Remove in deinit or use modern API
class ViewController: UIViewController {
    private var observer: NSObjectProtocol?

    override func viewDidLoad() {
        super.viewDidLoad()
        observer = NotificationCenter.default.addObserver(
            forName: .userLoggedIn,
            object: nil,
            queue: .main
        ) { [weak self] _ in
            self?.handleNotification()
        }
    }

    deinit {
        if let observer { NotificationCenter.default.removeObserver(observer) }
    }
}

Image Loading

NEVER load full resolution for thumbnails:

// ❌ 4000×3000 image for 80×80 thumbnail
let image = UIImage(contentsOfFile: path)  // Full resolution in memory!
imageView.image = image

// ✅ Downsample to display size
func downsampledImage(at url: URL, to size: CGSize) -> UIImage? {
    let options: [CFString: Any] = [
        kCGImageSourceShouldCache: false,
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height) * UIScreen.main.scale
    ]

    guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
          let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
        return nil
    }
    return UIImage(cgImage: cgImage)
}

NEVER cache images without limits:

// ❌ Unbounded memory growth
class ImageLoader {
    private var cache: [URL: UIImage] = [:]  // Grows forever!

    func image(for url: URL) -> UIImage? {
        if let cached = cache[url] { return cached }
        let image = loadImage(url)
        cache[url] = image  // Never evicted
        return image
    }
}

// ✅ Use NSCache with limits
class ImageLoader {
    private let cache = NSCache<NSURL, UIImage>()

    init() {
        cache.countLimit = 100
        cache.totalCostLimit = 50 * 1024 * 1024  // 50 MB
    }

    func image(for url: URL) -> UIImage? {
        if let cached = cache.object(forKey: url as NSURL) { return cached }
        guard let image = loadImage(url) else { return nil }
        cache.setObject(image, forKey: url as NSURL, cost: image.jpegData(compressionQuality: 1)?.count ?? 0)
        return image
    }
}

Heavy Operations

NEVER do heavy work on main thread:

// ❌ UI frozen during processing
func loadData() {
    let data = try! Data(contentsOf: largeFileURL)  // Blocks main thread!
    let parsed = parseData(data)  // Still blocking!
    self.items = parsed
}

// ✅ Use background thread, update on main
func loadData() async {
    let items = await Task.detached(priority: .userInitiated) {
        let data = try! Data(contentsOf: largeFileURL)
        return parseData(data)
    }.value

    await MainActor.run {
        self.items = items
    }
}

Essential Patterns

Efficient List View

struct EfficientListView: View {
    let items: [Item]

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 12) {  // Lazy = on-demand creation
                ForEach(items) { item in
                    ItemRow(item: item)
                        .id(item.id)  // Stable identity
                }
            }
        }
    }
}

// Equatable row prevents unnecessary updates
struct ItemRow: View, Equatable {
    let item: Item

    var body: some View {
        HStack {
            AsyncImage(url: item.imageURL) { image in
                image.resizable().aspectRatio(contentMode: .fill)
            } placeholder: {
                Color.gray.opacity(0.3)
            }
            .frame(width: 60, height: 60)
            .clipShape(RoundedRectangle(cornerRadius: 8))

            VStack(alignment: .leading) {
                Text(item.title).font(.headline)
                Text(item.subtitle).font(.caption).foregroundColor(.secondary)
            }
        }
    }

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

Memory-Safe ViewModel

@MainActor
final class ViewModel: ObservableObject {
    @Published private(set) var items: [Item] = []
    @Published private(set) var isLoading = false

    private var cancellables = Set<AnyCancellable>()
    private var loadTask: Task<Void, Never>?

    func load() {
        loadTask?.cancel()  // Cancel previous

        loadTask = Task {
            guard !Task.isCancelled else { return }

            isLoading = true
            defer { isLoading = false }

            do {
                let items = try await API.fetchItems()
                guard !Task.isCancelled else { return }
                self.items = items
            } catch {
                // Handle error
            }
        }
    }

    deinit {
        loadTask?.cancel()
        cancellables.removeAll()
    }
}

Debounced Search

@MainActor
final class SearchViewModel: ObservableObject {
    @Published var searchText = ""
    @Published private(set) var results: [Item] = []

    private var searchTask: Task<Void, Never>?

    init() {
        // Debounce search
        $searchText
            .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
            .removeDuplicates()
            .sink { [weak self] text in
                self?.performSearch(text)
            }
            .store(in: &cancellables)
    }

    private func performSearch(_ query: String) {
        searchTask?.cancel()

        guard !query.isEmpty else {
            results = []
            return
        }

        searchTask = Task {
            do {
                let results = try await API.search(query: query)
                guard !Task.isCancelled else { return }
                self.results = results
            } catch {
                // Handle error
            }
        }
    }
}

Quick Reference

Instruments Selection

Issue Instrument What to Look For
Slow UI Time Profiler Heavy main thread work
Memory leak Leaks Leaked objects
Memory growth Allocations Growing categories
Battery Energy Log Wake frequency
Network Network Request count, size
Disk File Activity Excessive I/O
GPU Core Animation Offscreen renders

SwiftUI Performance Checklist

Issue Solution
Slow list scrolling Use LazyVStack/LazyVGrid
All items re-render Stable IDs, Equatable rows
Heavy body computation Move to ViewModel
Cascading @Published updates Split or use computed
Animation jank Use drawingGroup()

Memory Management

Pattern Prevent Issue
[weak self] in closures Retain cycles
Timer.invalidate() in deinit Timer leaks
Remove observers in deinit Observer leaks
NSCache with limits Unbounded cache growth
Image downsampling Memory spikes

os_signpost for Custom Profiling

import os.signpost

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

os_signpost(.begin, log: log, name: "DataProcessing")
// Expensive work
os_signpost(.end, log: log, name: "DataProcessing")

Red Flags

Smell Problem Fix
Indices as List IDs Views recreated on mutation Use stable identifiers
Expensive body computation Runs every render Move to ViewModel
@StateObject for passed object Creates new instance Use @ObservedObject
Strong self in Timer/closure Retain cycle Use [weak self]
Full-res images for thumbnails Memory explosion Downsample to display size
Unbounded dictionary cache Memory growth Use NSCache with limits
Heavy work without Task.detached Blocks main thread Use background priority
Weekly Installs
15
GitHub Stars
6
First Seen
Jan 25, 2026
Installed on
codex11
opencode11
claude-code11
gemini-cli10
github-copilot10
antigravity9