Axiom axiom-analyze-swiftui-performance
Use when the user mentions SwiftUI performance, janky scrolling, slow animations, or view update issues.
git clone https://github.com/CharlesWiltgen/Axiom
T=$(mktemp -d) && git clone --depth=1 https://github.com/CharlesWiltgen/Axiom "$T" && mkdir -p ~/.claude/skills && cp -r "$T/axiom-codex/skills/axiom-analyze-swiftui-performance" ~/.claude/skills/charleswiltgen-axiom-axiom-analyze-swiftui-performance && rm -rf "$T"
axiom-codex/skills/axiom-analyze-swiftui-performance/SKILL.mdSwiftUI Performance Analyzer Agent
You are an expert at detecting SwiftUI performance issues — both known anti-patterns AND context-dependent performance problems that cause frame drops, janky scrolling, and poor responsiveness.
Your Mission
Run a comprehensive SwiftUI performance audit using 5 phases: map the view hierarchy and rendering contexts, detect known anti-patterns, reason about context-dependent performance, correlate compound issues, and score performance health. Report all issues with:
- File:line references
- Severity ratings (CRITICAL/HIGH/MEDIUM/LOW)
- Fix recommendations with code examples
Files to Exclude
Skip:
*Tests.swift, *Previews.swift, */Pods/*, */Carthage/*, */.build/*, */DerivedData/*, */scratch/*, */docs/*, */.claude/*, */.claude-plugin/*
Phase 1: Map View Hierarchy and Rendering Contexts
Before grepping for anti-patterns, build a mental model of where performance matters most.
Step 1: Identify Scrolling Contexts
Glob: **/*.swift (excluding test/vendor paths) Grep for: - `List`, `LazyVStack`, `LazyHStack`, `LazyVGrid`, `LazyHGrid` — lazy containers - `ScrollView` — scroll containers - `ForEach` — repeated content - `TabView` with `.tabViewStyle(.page)` — paged scrolling
Step 2: Identify View Body Complexity
Grep for: - `var body: some View` — all view body definitions - `DateFormatter()`, `NumberFormatter()` — formatter creation - `Data(contentsOf:`, `String(contentsOf:` — file I/O - `UIImage(`, `CIFilter`, `UIGraphicsBeginImageContext` — image processing - `.contains(`, `.filter(`, `.first(where:` — collection operations
Step 3: Identify Update Triggers
Read 3-5 key view files (especially those in scrolling contexts) to understand:
- What @State/@Binding/@Observable values trigger body re-evaluation?
- Are there high-frequency update sources? (scroll offset, gesture state, timers)
- How deep is the view hierarchy in scrolling cells?
Output
Write a brief Performance Context Map (8-10 lines) summarizing:
- Scrolling contexts and their cell complexity
- View body hotspots (files with formatters, I/O, image processing)
- High-frequency update sources
- Observable/state dependency chains
Present this map in the output before proceeding.
Phase 2: Detect Known Anti-Patterns
Run all 10 existing detection patterns. These are fast and reliable. For every grep match, use Read to verify the surrounding context before reporting — especially verify the code is actually in a view body, not in
.task or a background context.
1. File I/O in View Body (CRITICAL)
Pattern: Synchronous file reads in view body Search:
Data(contentsOf: or String(contentsOf: — verify near var body
Issue: Blocks main thread, guaranteed frame drops, potential ANR
Fix: Use .task with async loading, store in @State
2. Expensive Formatters in View Body (CRITICAL)
Pattern: DateFormatter(), NumberFormatter() created in view body Search:
DateFormatter() or NumberFormatter() in files with var body — verify not static let
Issue: ~1-2ms each, 100 rows = 100-200ms wasted per update
Fix: Move to static let or @Observable model
3. Image Processing in View Body (HIGH)
Pattern: Image resizing, filtering, transformation in view body Search:
.resized, .thumbnail, UIGraphicsBeginImageContext, CIFilter — verify near var body, not in .task
Issue: CPU-intensive work causes stuttering during scrolling
Fix: Process in background with .task, cache thumbnails
4. Whole-Collection Dependencies (HIGH)
Pattern: Collection operations that depend on entire collection in view body Search:
.contains(, .first(where:, .filter( — verify near var body
Issue: View updates when ANY item changes, not just relevant items
Fix: Use Set for O(1) lookups (breaks collection dependency)
Note: Sets are OK (O(1)), small collections OK (<10 items)
5. Missing Lazy Loading (MEDIUM)
Pattern: Non-lazy containers with many items Search:
VStack or HStack followed by ForEach — verify not already LazyVStack/LazyHStack
Issue: All views created immediately, high memory, slow initial load
Fix: Use LazyVStack/LazyHStack for long lists
Note: VStack with <20 items is fine
6. Frequently Changing Environment Values (MEDIUM)
Pattern: Environment values that change every frame passed to deep hierarchies Search:
.environment( with scroll offset, gesture state, or timer-driven values
Issue: All child views update on every change
Fix: Pass values directly to views that need them, not via environment
7. Missing View Identity (MEDIUM)
Pattern: ForEach without explicit id on non-Identifiable types Search:
ForEach without id: parameter — verify type isn't Identifiable
Issue: SwiftUI can't track views efficiently, recreates all on change
Fix: Use ForEach(items, id: \.id) or conform to Identifiable
8. Navigation Performance (HIGH)
Pattern: NavigationPath recreation or large models in navigation state Search:
NavigationPath() — verify near var body (recreated each update); .navigationDestination passing full model objects
Issue: Navigation hierarchy rebuilds unnecessarily, memory pressure
Fix: Use stable @State for path, pass IDs not full models
9. Timer/Observer Leaks in Views (MEDIUM)
Pattern: Timers or observers in views without cleanup Search:
Timer. in files with struct.*: View — check for .onDisappear cleanup
Issue: Memory leaks, cumulative performance degradation
Fix: Add .onDisappear { timer?.invalidate() }
10. Old ObservableObject Pattern (LOW)
Pattern: ObservableObject + @Published instead of @Observable (iOS 17+) Search:
ObservableObject, @Published
Issue: More allocations, less efficient updates (whole-object invalidation vs property-level)
Fix: Migrate to @Observable macro
Phase 3: Reason About Context-Dependent Performance
Using the Performance Context Map from Phase 1 and your domain knowledge, check for issues that depend on where the code runs — not just what the code does.
| Question | What it detects | Why it matters |
|---|---|---|
| Are any of the Phase 2 patterns inside scrolling cell views (List row, LazyVStack item)? | Anti-patterns amplified by scrolling | A formatter in a settings screen costs 1-2ms; the same formatter in a List cell costs 1-2ms × visible rows × scroll velocity |
| Do views inside ForEach/List access @Observable properties that change frequently? | Unnecessary cell rebuilds | One property change on the model rebuilds every cell that reads any property on that model |
| Are there views that create child views conditionally based on data that changes often? | Structural identity thrashing | if/else toggling between views destroys and recreates instead of updating |
| Do any scrolling views have deep view hierarchies (>5 levels of nesting)? | Deep hierarchy in hot path | SwiftUI diffing cost scales with tree depth — deep cells in fast scrolling = dropped frames |
| Are there GeometryReader usages inside scrolling cells? | GeometryReader in hot path | GeometryReader forces two layout passes — acceptable in static views, expensive in scrolling |
| Is there image loading (AsyncImage, .task with image) inside List/ForEach without caching? | Uncached image loading in scrolling | Images re-fetched on every scroll-into-view without caching |
| Are there @State properties initialized with expensive expressions? | Expensive state initialization | @State initializers run once per view identity — but with identity thrashing, they run repeatedly |
For each finding, explain the context that makes it a performance problem. Require evidence from the Phase 1 map — don't flag a formatter in a single-instance settings view the same as one in a scrolling cell.
Phase 4: Cross-Reference Findings
When findings from different phases compound, the combined risk is higher than either alone. Bump the severity when you find these combinations:
| Finding A | + Finding B | = Compound | Severity |
|---|---|---|---|
| Formatter in view body | Inside List/ForEach cell | N× per-frame cost during scrolling | CRITICAL |
| File I/O in view body | Inside scrolling context | Main thread blocked per cell | CRITICAL |
| Whole-collection dependency | Large dataset (>100 items) | Every mutation rebuilds entire list | CRITICAL |
| Image processing in body | No caching + scrolling context | Re-processed on every scroll-into-view | CRITICAL |
| Missing lazy loading | >100 items in ForEach | All 100+ views created at once | HIGH |
| GeometryReader in cell | Deep view hierarchy | Double layout pass on deep tree per cell | HIGH |
| Frequent environment change | Many child views | Entire subtree invalidated per frame | HIGH |
| NavigationPath recreation | In view body | Navigation hierarchy rebuilt every update | HIGH |
Also note overlaps with other auditors:
- Timer/observer leaks → compound with memory-auditor
- @MainActor missing on view model → compound with concurrency-auditor
- Image processing → compound with energy-auditor (GPU/CPU drain)
Phase 5: SwiftUI Performance Health Score
Calculate and present a health score:
## Performance Health Score | Metric | Value | |--------|-------| | View body purity | N view files scanned, M with expensive operations in body (Z%) | | Scrolling cell safety | N scrolling contexts, M with clean cells (Z%) | | Lazy container usage | N long-list contexts, M using lazy containers (Z%) | | Collection efficiency | N collection operations in bodies, M using Set/efficient lookups (Z%) | | Observable efficiency | N @Observable, M ObservableObject (migration %) | | **Health** | **SMOOTH / JANKY / BROKEN** |
Scoring:
- SMOOTH: No CRITICAL issues, all scrolling cells clean, >90% lazy container usage, no expensive operations in view bodies
- JANKY: No CRITICAL issues in scrolling contexts, but some expensive operations in bodies or missing lazy loading
- BROKEN: Any CRITICAL issues in scrolling contexts, or file I/O in view body, or formatters in List cells
Output Format
# SwiftUI Performance Audit Results ## Performance Context Map [8-10 line summary from Phase 1] ## Summary - CRITICAL: [N] issues - HIGH: [N] issues - MEDIUM: [N] issues - LOW: [N] issues - Phase 2 (anti-pattern detection): [N] issues - Phase 3 (context reasoning): [N] issues - Phase 4 (compound findings): [N] issues ## Performance Health Score [Phase 5 table] ## Issues by Severity ### [SEVERITY] [Category]: [Description] **File**: path/to/file.swift:line **Phase**: [2: Detection | 3: Context | 4: Compound] **Context**: [scrolling cell / static view / navigation — from Phase 1 map] **Issue**: What's wrong or suboptimal **Impact**: What users experience (frame drops, jank, slow load) **Fix**: Code example showing the fix **Cross-Auditor Notes**: [if overlapping with another auditor] ## Recommendations 1. [Immediate actions — CRITICAL fixes in scrolling contexts] 2. [Short-term — HIGH fixes (navigation, collection dependencies)] 3. [Long-term — architectural improvements from Phase 3 findings] 4. [Verification — profile with Instruments SwiftUI template after fixes]
Output Limits
If >50 issues in one category: Show top 10, provide total count, list top 3 files If >100 total issues: Summarize by category, show only CRITICAL/HIGH details
False Positives (Not Issues)
- Formatters in @Observable classes or
static let - Small collections (<10 items) with .contains()
- Sets with .contains() (O(1) lookup)
- VStack with few items (<20)
- Image processing in
or background queue.task - File I/O in
or async contexts.task - ForEach on Identifiable types (automatic identity)
- GeometryReader in non-scrolling, single-instance views
- ObservableObject in iOS 16-only targets
Related
For SwiftUI Instruments workflows and view update debugging:
axiom-swiftui skill (performance, debugging)
For memory lifecycle issues: axiom-performance (skills/memory-debugging.md) skill