home / skills / rshankras / claude-code-apple-skills / swiftui-debugging
This skill helps diagnose and fix SwiftUI performance issues by pinpointing unnecessary re-renders, identity problems, and slow body evaluations.
npx playbooks add skill rshankras/claude-code-apple-skills --skill swiftui-debuggingReview the files below or copy the command above to add this skill to your agents.
---
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) |
This skill diagnoses SwiftUI performance problems and guides targeted fixes for unnecessary re-renders, view identity issues, slow body evaluations, and lazy-loading mistakes. It helps you find what is triggering view updates, reduce jank in lists and grids, and apply concrete workarounds for common pitfalls. Use it to systematically narrow performance hotspots and validate fixes with Instruments or signposts.
The skill walks through a decision tree based on observed symptoms (excessive re-renders, choppy scrolling, lost state, or unknown slowness). It recommends lightweight instrumentation (Self._printChanges(), os_signpost) and profiling with the SwiftUI Instruments template to see which bodies and properties change. It then maps findings to concrete fixes: stabilize view identity, restrict observation scope, introduce lazy containers, avoid allocations in body, and prefer efficient APIs.
How do I quickly see what caused a view to re-evaluate?
Insert let _ = Self._printChanges() in the view body. The console shows which @self, @identity, or property changed and triggered the re-evaluation.
When should I use LazyVStack vs VStack?
Use LazyVStack for large or unbounded collections or when many cells could be offscreen. A plain VStack evaluates all children eagerly and can cause memory and CPU spikes.
Is AnyView always bad for performance?
AnyView adds type erasure that can defeat SwiftUI diffing. Prefer @ViewBuilder, Group, or explicit generics. Use AnyView only when necessary and measure impact.