--- name: swiftui-debugging description: Diagnose SwiftUI performance issues including unnecessary re-renders, view identity problems, and slow body evaluations. Use when SwiftUI views are slow, janky, or re-rendering too often. allowed-tools: [Read, Glob, Grep] --- # SwiftUI Performance Debugging Systematic guide for diagnosing and fixing SwiftUI performance problems: unnecessary view re-evaluations, identity issues, expensive body computations, and lazy loading mistakes. ## When This Skill Activates Use this skill when the user: - Reports slow or janky SwiftUI views - Sees excessive view re-renders or body re-evaluations - Asks about `Self._printChanges()` or view debugging - Has scrolling performance issues with lists or grids - Asks why a view keeps updating when nothing changed - Mentions `@Observable` or `ObservableObject` performance differences - Wants to understand SwiftUI view identity or diffing - Uses `AnyView` and asks about performance implications - Has a hang or stutter traced to SwiftUI rendering ## Decision Tree ``` What SwiftUI performance problem are you seeing? | +- Views re-render when they should not | +- Read body-reevaluation.md | +- Self._printChanges() to identify which property changed | +- @Observable vs ObservableObject observation differences | +- Splitting views to narrow observation scope | +- Scrolling is slow / choppy (lists, grids) | +- Read lazy-loading.md | +- VStack vs LazyVStack, ForEach without lazy container | +- List prefetching, grid cell reuse | +- Views lose state unexpectedly / animate when they should not | +- Read view-identity.md | +- Structural vs explicit identity | +- .id() misuse, conditional view branching | +- Known pitfall (AnyView, DateFormatter in body, etc.) | +- Read common-pitfalls.md | +- AnyView type erasure, object creation in body | +- Over-observation, expensive computations | +- General "my SwiftUI app is slow" (unknown cause) | +- Start with body-reevaluation.md, then common-pitfalls.md | +- Use Instruments SwiftUI template (see Debugging Tools below) ``` ## API Availability | API / Technique | Minimum Version | Reference | |----------------|-----------------|-----------| | `Self._printChanges()` | iOS 15 | body-reevaluation.md | | `@Observable` | iOS 17 / macOS 14 | body-reevaluation.md | | `@ObservableObject` | iOS 13 | body-reevaluation.md | | `LazyVStack` / `LazyHStack` | iOS 14 | lazy-loading.md | | `LazyVGrid` / `LazyHGrid` | iOS 14 | lazy-loading.md | | `.id()` modifier | iOS 13 | view-identity.md | | Instruments SwiftUI template | Xcode 14+ | SKILL.md | | `os_signpost` | iOS 12 | SKILL.md | ## Top 5 Mistakes -- Quick Reference | # | Mistake | Fix | Details | |---|---------|-----|---------| | 1 | Large `ForEach` inside `VStack` or `ScrollView` without lazy container | Wrap in `LazyVStack` -- eager `VStack` creates all views upfront | lazy-loading.md | | 2 | Using `AnyView` to erase types | Use `@ViewBuilder`, `Group`, or concrete generic types -- `AnyView` defeats diffing | common-pitfalls.md | | 3 | Creating objects in `body` (`DateFormatter()`, `NumberFormatter()`) | Use `static let` shared instances or `@State` for mutable objects | common-pitfalls.md | | 4 | Observing entire model when only one property is needed | Split into smaller `@Observable` objects or extract subviews | body-reevaluation.md | | 5 | Unstable `.id()` values causing full view recreation every render | Use stable identifiers (database IDs, UUIDs), never array indices or random values | view-identity.md | ## Debugging Tools ### Self._printChanges() Add to any view body to see what triggered re-evaluation: ```swift var body: some View { let _ = Self._printChanges() // ... view content } ``` Output reads: `ViewName: @self, @identity, _propertyName changed.` See body-reevaluation.md for full interpretation guide. ### Instruments SwiftUI Template 1. **Xcode > Product > Profile** (Cmd+I) 2. Choose **SwiftUI** template (includes View Body, View Properties, Core Animation Commits) 3. Record, reproduce the slow interaction, stop 4. **View Body** lane shows which views had their body evaluated and how often 5. **View Properties** lane shows which properties changed ### os_signpost for Custom Measurement ```swift import os private let perfLog = OSLog(subsystem: "com.app.perf", category: "SwiftUI") var body: some View { let _ = os_signpost(.event, log: perfLog, name: "MyView.body") // ... view content } ``` View in Instruments with the **os_signpost** instrument to count body evaluations per second. ## Review Checklist ### View Identity - [ ] No unstable `.id()` values (random, Date(), array index on mutable arrays) - [ ] Conditional branches (`if`/`else`) do not cause unnecessary view destruction - [ ] `ForEach` uses stable, unique identifiers from the model ### Body Re-evaluation - [ ] Views observe only the properties they actually use - [ ] `@Observable` classes preferred over `ObservableObject` (iOS 17+) - [ ] No unnecessary `@State` changes that trigger body re-evaluation - [ ] Large views split into smaller subviews to narrow observation scope ### Lazy Loading - [ ] Large collections use `LazyVStack` / `LazyHStack`, not `VStack` / `HStack` - [ ] `List` or lazy stack used for 50+ items - [ ] No `.frame(maxHeight: .infinity)` on children inside lazy containers (defeats laziness) ### Common Pitfalls - [ ] No `AnyView` type erasure (use `@ViewBuilder` or `Group`) - [ ] No object allocation in `body` (`DateFormatter`, `NSPredicate`, view models) - [ ] Expensive computations moved to background with `task { }` or `Task.detached` - [ ] Images use `AsyncImage` or `.resizable()` with proper sizing, not raw `UIImage` decoding in body ## Reference Files | File | Content | |------|---------| | [view-identity.md](view-identity.md) | Structural vs explicit identity, `.id()` usage, conditional branching | | [body-reevaluation.md](body-reevaluation.md) | What triggers body, `_printChanges()`, `@Observable` vs `ObservableObject` | | [lazy-loading.md](lazy-loading.md) | Lazy vs eager containers, `List`, `ForEach`, grid performance | | [common-pitfalls.md](common-pitfalls.md) | `AnyView`, object creation in body, over-observation, expensive computations | | [../profiling/SKILL.md](../profiling/SKILL.md) | General Instruments profiling (Time Profiler, Memory, Energy) |