Agent skill
swiftui-debugging
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.
Stars
127
Forks
10
Install this agent skill to your Project
npx add-skill https://github.com/rshankras/claude-code-apple-skills/tree/main/skills/performance/swiftui-debugging
SKILL.md
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
@ObservableorObservableObjectperformance differences - Wants to understand SwiftUI view identity or diffing
- Uses
AnyViewand 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
- Xcode > Product > Profile (Cmd+I)
- Choose SwiftUI template (includes View Body, View Properties, Core Animation Commits)
- Record, reproduce the slow interaction, stop
- View Body lane shows which views had their body evaluated and how often
- 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 -
ForEachuses stable, unique identifiers from the model
Body Re-evaluation
- Views observe only the properties they actually use
-
@Observableclasses preferred overObservableObject(iOS 17+) - No unnecessary
@Statechanges that trigger body re-evaluation - Large views split into smaller subviews to narrow observation scope
Lazy Loading
- Large collections use
LazyVStack/LazyHStack, notVStack/HStack -
Listor lazy stack used for 50+ items - No
.frame(maxHeight: .infinity)on children inside lazy containers (defeats laziness)
Common Pitfalls
- No
AnyViewtype erasure (use@ViewBuilderorGroup) - No object allocation in
body(DateFormatter,NSPredicate, view models) - Expensive computations moved to background with
task { }orTask.detached - Images use
AsyncImageor.resizable()with proper sizing, not rawUIImagedecoding in body
Reference Files
| File | Content |
|---|---|
| view-identity.md | Structural vs explicit identity, .id() usage, conditional branching |
| body-reevaluation.md | What triggers body, _printChanges(), @Observable vs ObservableObject |
| lazy-loading.md | Lazy vs eager containers, List, ForEach, grid performance |
| common-pitfalls.md | AnyView, object creation in body, over-observation, expensive computations |
| ../profiling/SKILL.md | General Instruments profiling (Time Profiler, Memory, Energy) |
Didn't find tool you were looking for?