$npx -y skills add Livsy90/iOS-Performance-Agent-Skills --skill swiftui-performanceUse this skill when reviewing or fixing SwiftUI performance issues, including unnecessary invalidation, unstable identity, broad state dependencies, expensive body work, heavy rows, scrolling hitches, layout/drawing cost, or async lifecycle work. Do not use it for general SwiftUI
| 1 | # SwiftUI Performance |
| 2 | |
| 3 | ## Purpose |
| 4 | |
| 5 | Use this skill to review and refactor SwiftUI code with a performance-first mental model focused on change locality. |
| 6 | |
| 7 | SwiftUI performance work should explain which change invalidates which part of the UI, why that work matters, and how to keep updates local. |
| 8 | |
| 9 | ## Scope |
| 10 | |
| 11 | Use this skill for: |
| 12 | |
| 13 | * SwiftUI screen, list, row, layout, drawing, animation, and lifecycle reviews |
| 14 | * targeted refactors that reduce unnecessary updates or rendering work |
| 15 | * profiling plans only when they validate a SwiftUI performance hypothesis |
| 16 | |
| 17 | Do not use this skill for: |
| 18 | |
| 19 | * UIKit-only performance problems |
| 20 | * app launch performance unless the issue is inside SwiftUI root view or scene construction |
| 21 | * networking, backend latency, database performance, or caching unless they affect SwiftUI updates |
| 22 | * generic Instruments walkthroughs without a SwiftUI code path |
| 23 | * broad architecture rewrites without a concrete SwiftUI performance risk |
| 24 | |
| 25 | ## Core Model |
| 26 | |
| 27 | Answer three questions first: |
| 28 | |
| 29 | 1. What changed? |
| 30 | 2. Which views depend on that change? |
| 31 | 3. How much UI work happens because of it? |
| 32 | |
| 33 | Then inspect: |
| 34 | |
| 35 | * Identity: does SwiftUI preserve the same logical view across updates? |
| 36 | * Lifetime: is state owned by the smallest stable component that needs it? |
| 37 | * Dependencies: does each view read only the data it needs to render? |
| 38 | * Rendering: is `body` cheap, predictable, and free from heavy transformation work? |
| 39 | * Lifecycle: is async work tied to `.task`, `.onAppear`, or explicit actions rather than started from `body`? |
| 40 | |
| 41 | A SwiftUI view is a value description of UI. Do not treat it as a long-lived UIKit-style object. |
| 42 | |
| 43 | ## Review Workflow |
| 44 | |
| 45 | Inspect the smallest relevant code path before suggesting broad changes. |
| 46 | |
| 47 | Check in this order: |
| 48 | |
| 49 | 1. Stable and intentional identity. |
| 50 | 2. Expensive work in `body`. |
| 51 | 3. Broad state or model reads. |
| 52 | 4. State ownership and lifetime. |
| 53 | 5. List, row, and pagination structure. |
| 54 | 6. Closure-heavy inputs, custom bindings, and unnecessary type erasure. |
| 55 | 7. Layout, drawing, and animation cost in repeated content. |
| 56 | 8. Async lifecycle, cancellation, and main-actor work. |
| 57 | 9. Profiling or debug probes when confirmation is useful. |
| 58 | |
| 59 | Prefer targeted refactors over architectural rewrites. |
| 60 | |
| 61 | ## Evidence Rule |
| 62 | |
| 63 | Always separate: |
| 64 | |
| 65 | * static code review findings |
| 66 | * likely risks |
| 67 | * hypotheses |
| 68 | * measured results |
| 69 | * user-provided evidence |
| 70 | * tool-generated evidence |
| 71 | |
| 72 | Never invent timing numbers or claim profiling results unless the user provided evidence or a profiling command was actually run. |
| 73 | |
| 74 | Prefer: |
| 75 | |
| 76 | > This parent view reads the whole model, so changes to that model can invalidate a large part of the screen. |
| 77 | |
| 78 | Avoid: |
| 79 | |
| 80 | > This costs 500 ms. |
| 81 | |
| 82 | ## Common Red Flags |
| 83 | |
| 84 | Flag these patterns when they appear on an update or scrolling path: |
| 85 | |
| 86 | * unstable IDs such as `.id(UUID())` or `var id: UUID { UUID() }` |
| 87 | * index-based identity for mutable collections |
| 88 | * sorting, filtering, grouping, formatting, parsing, decoding, or database reads inside `body` |
| 89 | * filtering inside `ForEach` instead of preparing visible rows first |
| 90 | * passing one giant model into every child view |
| 91 | * broad computed properties that hide many state reads |
| 92 | * `AnyView` in large repeated collections |
| 93 | * custom `Binding(get:set:)` when a key-path binding would work |
| 94 | * many stored non-visual closures in row views |
| 95 | * `GeometryReader`, preferences, heavy shadows, masks, blurs, or overlays in every row |
| 96 | * async work started directly from `body` |
| 97 | * unguarded `.onAppear` pagination triggers inside rows |
| 98 | |
| 99 | ## Refactor Guidelines |
| 100 | |
| 101 | Use the smallest fix that addresses the cause: |
| 102 | |
| 103 | * keep identity stable |
| 104 | * move expensive transformations out of `body` |
| 105 | * prepare render-ready models outside repeated rendering paths |
| 106 | * move state closer to the component that owns it |
| 107 | * split large views into dependency-focused subviews |
| 108 | * simplify row inputs and repeated structure |
| 109 | * prefer key-path bindings over custom closure bindings when no transformation is needed |
| 110 | * use `.equatable()` only when visual equality is clear and cheaper than recomputing |
| 111 | * use `.task(id:)` when work is tied to a changing input and should cancel/restart automatically |
| 112 | * consider UIKit only for genuinely hot paths that need lower-level control |
| 113 | |
| 114 | Do not use `.equatable()`, memoization, or caching as blanket fixes. Explain the invalidation or rendering issue first. |
| 115 | |
| 116 | ## Reference Routing |
| 117 | |
| 118 | Read references only when the task needs more detail: |
| 119 | |
| 120 | * `references/identity-and-state.md` — identity, `.id`, `ForEach`, `@State`, `@StateObject`, `@ObservedObject`, owned observable models |
| 121 | * `references/observation-and-dependencies.md` — Observation, `ObservableOb |