Claude
Skills
Sign in
Back

swiftui-performance

Included with Lifetime
$97 forever

Audit and improve SwiftUI runtime performance. Use when diagnosing slow rendering, janky scrolling, high CPU, memory usage, excessive view updates, layout thrash, body evaluation cost, identity churn, view lifetime issues, lazy loading, Instruments profiling guidance, and performance audit requests.

Security

What this skill does


# SwiftUI Performance

Audit SwiftUI view performance end-to-end, from instrumentation and baselining to root-cause analysis and concrete remediation steps.

## Contents

- [Workflow Decision Tree](#workflow-decision-tree)
- [1. Code-First Review](#1-code-first-review)
- [2. Guide the User to Profile](#2-guide-the-user-to-profile)
- [3. Analyze and Diagnose](#3-analyze-and-diagnose)
- [4. Remediate](#4-remediate)
- [Common Code Smells (and Fixes)](#common-code-smells-and-fixes)
- [5. Verify](#5-verify)
- [Outputs](#outputs)
- [Instruments Profiling](#instruments-profiling)
- [Identity and Lifetime](#identity-and-lifetime)
- [Lazy Loading Patterns](#lazy-loading-patterns)
- [State and Observation Optimization](#state-and-observation-optimization)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)

## Workflow Decision Tree

- If the user provides code, start with "Code-First Review."
- If the user only describes symptoms, ask for minimal code/context, then do "Code-First Review."
- If code review is inconclusive, go to "Guide the User to Profile" and ask for a trace or screenshots.

## 1. Code-First Review

Collect:
- Target view/feature code.
- Data flow: state, environment, observable models.
- Symptoms and reproduction steps.

Focus on:
- View invalidation storms from broad state changes.
- Unstable identity in lists (`id` churn, `UUID()` per render).
- Top-level conditional view swapping (`if/else` returning different root branches).
- Heavy work in `body` (formatting, sorting, image decoding).
- Layout thrash (deep stacks, `GeometryReader`, preference chains).
- Large images without downsampling or resizing.
- Over-animated hierarchies (implicit animations on large trees).

Provide:
- Likely root causes with code references.
- Suggested fixes and refactors.
- If needed, a minimal repro or instrumentation suggestion.

## 2. Guide the User to Profile

Explain how to collect data with Instruments:
- Use the SwiftUI template in Instruments.
- Profile a **Release build** on a real device when possible.
- Reproduce the exact interaction (scroll, navigation, animation).
- Capture SwiftUI timeline and Time Profiler.
- Export or screenshot the relevant lanes and the call tree.

Ask for:
- Trace export or screenshots of SwiftUI lanes + Time Profiler call tree.
- Device/OS/build configuration.

## 3. Analyze and Diagnose

Prioritize likely SwiftUI culprits:
- View invalidation storms from broad state changes.
- Unstable identity in lists (`id` churn, `UUID()` per render).
- Top-level conditional view swapping (`if/else` returning different root branches).
- Heavy work in `body` (formatting, sorting, image decoding).
- Layout thrash (deep stacks, `GeometryReader`, preference chains).
- Large images without downsampling or resizing.
- Over-animated hierarchies (implicit animations on large trees).

Summarize findings with evidence from traces/logs.

## 4. Remediate

Apply targeted fixes:
- Narrow state scope (`@State`/`@Observable` closer to leaf views).
- Stabilize identities for `ForEach` and lists.
- Move heavy work out of `body` (precompute, cache, `@State`).
- Use `equatable()` or value wrappers for expensive subtrees.
- Downsample images before rendering.
- Reduce layout complexity or use fixed sizing where possible.

## Common Code Smells (and Fixes)

Look for these patterns during code review.

### Expensive formatters in `body`

```swift
var body: some View {
    let number = NumberFormatter() // slow allocation
    let measure = MeasurementFormatter() // slow allocation
    Text(measure.string(from: .init(value: meters, unit: .meters)))
}
```

Prefer cached formatters in a model or a dedicated helper:

```swift
final class DistanceFormatter {
    static let shared = DistanceFormatter()
    let number = NumberFormatter()
    let measure = MeasurementFormatter()
}
```

### Computed properties that do heavy work

```swift
var filtered: [Item] {
    items.filter { $0.isEnabled } // runs on every body eval
}
```

Prefer precompute or cache on change:

```swift
@State private var filtered: [Item] = []
// update filtered when inputs change
```

### Sorting/filtering in `body` or `ForEach`

```swift
// DON'T: sorts or filters on every body evaluation
ForEach(items.sorted(by: sortRule)) { item in Row(item) }
ForEach(items.filter { $0.isEnabled }) { item in Row(item) }
```

Prefer precomputed, cached collections with stable identity. Update on input change, not in `body`.

### Unstable identity

```swift
ForEach(items, id: \.self) { item in
    Row(item)
}
```

Avoid `id: \.self` for non-stable values; use a stable ID.

### Top-level conditional view swapping

```swift
var content: some View {
    if isEditing {
        editingView
    } else {
        readOnlyView
    }
}
```

Prefer one stable base view and localize conditions to sections/modifiers (for example inside `toolbar`, row content, `overlay`, or `disabled`). This reduces root identity churn and helps SwiftUI diffing stay efficient.

### Image decoding on the main thread

```swift
Image(uiImage: UIImage(data: data)!)
```

Prefer decode/downsample off the main thread and store the result.

### Broad dependencies in observable models

```swift
@Observable class Model {
    var items: [Item] = []
}

var body: some View {
    Row(isFavorite: model.items.contains(item))
}
```

Prefer granular view models or per-item state to reduce update fan-out.

## 5. Verify

Ask the user to re-run the same capture and compare with baseline metrics.
Summarize the delta (CPU, frame drops, memory peak) if provided.

## Outputs

Provide:
- A short metrics table (before/after if available).
- Top issues (ordered by impact).
- Proposed fixes with estimated effort.

## Instruments Profiling

Use the **SwiftUI instrument template** in Xcode (Cmd+I to profile). Key instruments: SwiftUI View Body (body evaluation counts), SwiftUI View Properties (state change tracking), Time Profiler, and Hangs.

Add `Self._printChanges()` in debug builds to log which property triggered a view update:

```swift
var body: some View {
    #if DEBUG
    let _ = Self._printChanges()  // "MyView: @self, _count changed."
    #endif
    Text("Count: \(count)")
}
```

See [references/optimizing-swiftui-performance-instruments.md](references/optimizing-swiftui-performance-instruments.md) for the full profiling workflow.

## Identity and Lifetime

### Structural Identity vs Explicit Identity

SwiftUI assigns every view an **identity** used to track its lifetime, state, and animations.

- **Structural identity** (default): determined by the view's position in the view hierarchy. SwiftUI uses the call-site location in `body` to distinguish views.
- **Explicit identity**: you assign with `.id(_:)` modifier or `ForEach(items, id: \.stableID)`.

```swift
// Structural identity: SwiftUI knows these are different views by position
VStack {
    Text("First")   // position 0
    Text("Second")  // position 1
}
```

### How Identity Tracks View Lifetime

When a view's identity changes, SwiftUI treats it as a **new view**:

- All `@State` is reset.
- `onAppear` fires again.
- Animations may restart.
- Transition animations play (if defined).

When identity stays the same, SwiftUI updates the **existing view** in place, preserving state and providing smooth transitions.

### AnyView and Identity Reset

`AnyView` erases type information, forcing SwiftUI to fall back to less efficient diffing:

```swift
// DON'T: AnyView destroys type identity
func makeView(for item: Item) -> AnyView {
    if item.isPremium {
        return AnyView(PremiumRow(item: item))
    } else {
        return AnyView(StandardRow(item: item))
    }
}

// DO: use @ViewBuilder to preserve structural identity
@ViewBuilder
func makeView(for item: Item) -> some View {
    if item.isPremium {
        PremiumRow(item: item)
    } else {
        StandardRow(item: item)
    }
}
```

`AnyView` also prevents SwiftUI from detecting which branch cha

Related in Security