SwiftUI Performance
Purpose
Use this skill to review and refactor SwiftUI code with a performance-first mental model focused on change locality.
SwiftUI performance work should explain which change invalidates which part of the UI, why that work matters, and how to keep updates local.
Scope
Use this skill for:
- SwiftUI screen, list, row, layout, drawing, animation, and lifecycle reviews
- targeted refactors that reduce unnecessary updates or rendering work
- profiling plans only when they validate a SwiftUI performance hypothesis
Do not use this skill for:
- UIKit-only performance problems
- app launch performance unless the issue is inside SwiftUI root view or scene construction
- networking, backend latency, database performance, or caching unless they affect SwiftUI updates
- generic Instruments walkthroughs without a SwiftUI code path
- broad architecture rewrites without a concrete SwiftUI performance risk
Core Model
Answer three questions first:
- What changed?
- Which views depend on that change?
- How much UI work happens because of it?
Then inspect:
- Identity: does SwiftUI preserve the same logical view across updates?
- Lifetime: is state owned by the smallest stable component that needs it?
- Dependencies: does each view read only the data it needs to render?
- Rendering: is
body cheap, predictable, and free from heavy transformation work?
- Lifecycle: is async work tied to
.task, .onAppear, or explicit actions rather than started from body?
A SwiftUI view is a value description of UI. Do not treat it as a long-lived UIKit-style object.
Review Workflow
Inspect the smallest relevant code path before suggesting broad changes.
Check in this order:
- Stable and intentional identity.
- Expensive work in
body.
- Broad state or model reads.
- State ownership and lifetime.
- List, row, and pagination structure.
- Closure-heavy inputs, custom bindings, and unnecessary type erasure.
- Layout, drawing, and animation cost in repeated content.
- Async lifecycle, cancellation, and main-actor work.
- Profiling or debug probes when confirmation is useful.
Prefer targeted refactors over architectural rewrites.
Evidence Rule
Always separate:
- static code review findings
- likely risks
- hypotheses
- measured results
- user-provided evidence
- tool-generated evidence
Never invent timing numbers or claim profiling results unless the user provided evidence or a profiling command was actually run.
Prefer:
This parent view reads the whole model, so changes to that model can invalidate a large part of the screen.
Avoid:
This costs 500 ms.
Common Red Flags
Flag these patterns when they appear on an update or scrolling path:
- unstable IDs such as
.id(UUID()) or var id: UUID { UUID() }
- index-based identity for mutable collections
- sorting, filtering, grouping, formatting, parsing, decoding, or database reads inside
body
- filtering inside
ForEach instead of preparing visible rows first
- passing one giant model into every child view
- broad computed properties that hide many state reads
AnyView in large repeated collections
- custom
Binding(get:set:) when a key-path binding would work
- many stored non-visual closures in row views
GeometryReader, preferences, heavy shadows, masks, blurs, or overlays in every row
- async work started directly from
body
- unguarded
.onAppear pagination triggers inside rows
Refactor Guidelines
Use the smallest fix that addresses the cause:
- keep identity stable
- move expensive transformations out of
body
- prepare render-ready models outside repeated rendering paths
- move state closer to the component that owns it
- split large views into dependency-focused subviews
- simplify row inputs and repeated structure
- prefer key-path bindings over custom closure bindings when no transformation is needed
- use
.equatable() only when visual equality is clear and cheaper than recomputing
- use
.task(id:) when work is tied to a changing input and should cancel/restart automatically
- consider UIKit only for genuinely hot paths that need lower-level control
Do not use .equatable(), memoization, or caching as blanket fixes. Explain the invalidation or rendering issue first.
Reference Routing
Read references only when the task needs more detail:
references/identity-and-state.md — identity, .id, ForEach, @State, @StateObject, @ObservedObject, owned observable models
references/observation-and-dependencies.md — Observation, ObservableObject, broad dependencies, environment reads
references/body-cost-and-render-models.md — sorting, filtering, formatting, derived data, render models
references/lists-pagination-and-rows.md — List, ScrollView, LazyVStack, rows, pagination, scrolling
references/closures-bindings-and-equatable.md — closures, bindings, .equatable(), action handlers
references/layout-drawing-and-animation.md — layout, drawing, modifiers, animation hitches
references/async-lifecycle-and-mainactor.md — .task, .onAppear, cancellation, MainActor
references/profiling-validation.md — Instruments, xctrace, signposts, XCTest, MetricKit
Response Format
For code reviews, answer with:
- Main issue
- Why it matters in SwiftUI
- Risk level
- Suggested refactor
- Code example when useful
- What to measure if confirmation is needed
For profiling requests, answer with:
- Scenario
- Hypothesis
- Tool or probe
- What to look for
- How to compare before and after
Keep the answer concrete. Do not say “SwiftUI is slow.” Explain the specific identity, dependency, rendering, layout, drawing, animation, or lifecycle issue.
Final Principle
SwiftUI performance improves when code makes change locality obvious.
1---2name: swiftui-performance3description: Use 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 syntax, styling, UIKit-only performance, or generic profiling unless SwiftUI update behavior is central.4---5
6# SwiftUI Performance
7
8## Purpose
9
10Use this skill to review and refactor SwiftUI code with a performance-first mental model focused on change locality.
11
12SwiftUI performance work should explain which change invalidates which part of the UI, why that work matters, and how to keep updates local.
13
14## Scope
15
16Use this skill for:
17
18* SwiftUI screen, list, row, layout, drawing, animation, and lifecycle reviews
19* targeted refactors that reduce unnecessary updates or rendering work
20* profiling plans only when they validate a SwiftUI performance hypothesis
21
22Do not use this skill for:
23
24* UIKit-only performance problems
25* app launch performance unless the issue is inside SwiftUI root view or scene construction
26* networking, backend latency, database performance, or caching unless they affect SwiftUI updates
27* generic Instruments walkthroughs without a SwiftUI code path
28* broad architecture rewrites without a concrete SwiftUI performance risk
29
30## Core Model
31
32Answer three questions first:
33
341. What changed?
352. Which views depend on that change?
363. How much UI work happens because of it?
37
38Then inspect:
39
40* Identity: does SwiftUI preserve the same logical view across updates?
41* Lifetime: is state owned by the smallest stable component that needs it?
42* Dependencies: does each view read only the data it needs to render?
43* Rendering: is `body` cheap, predictable, and free from heavy transformation work?
44* Lifecycle: is async work tied to `.task`, `.onAppear`, or explicit actions rather than started from `body`?
45
46A SwiftUI view is a value description of UI. Do not treat it as a long-lived UIKit-style object.
47
48## Review Workflow
49
50Inspect the smallest relevant code path before suggesting broad changes.
51
52Check in this order:
53
541. Stable and intentional identity.
552. Expensive work in `body`.
563. Broad state or model reads.
574. State ownership and lifetime.
585. List, row, and pagination structure.
596. Closure-heavy inputs, custom bindings, and unnecessary type erasure.
607. Layout, drawing, and animation cost in repeated content.
618. Async lifecycle, cancellation, and main-actor work.
629. Profiling or debug probes when confirmation is useful.
63
64Prefer targeted refactors over architectural rewrites.
65
66## Evidence Rule
67
68Always separate:
69
70* static code review findings
71* likely risks
72* hypotheses
73* measured results
74* user-provided evidence
75* tool-generated evidence
76
77Never invent timing numbers or claim profiling results unless the user provided evidence or a profiling command was actually run.
78
79Prefer:
80
81> This parent view reads the whole model, so changes to that model can invalidate a large part of the screen.
82
83Avoid:
84
85> This costs 500 ms.
86
87## Common Red Flags
88
89Flag these patterns when they appear on an update or scrolling path:
90
91* unstable IDs such as `.id(UUID())` or `var id: UUID { UUID() }`
92* index-based identity for mutable collections
93* sorting, filtering, grouping, formatting, parsing, decoding, or database reads inside `body`
94* filtering inside `ForEach` instead of preparing visible rows first
95* passing one giant model into every child view
96* broad computed properties that hide many state reads
97* `AnyView` in large repeated collections
98* custom `Binding(get:set:)` when a key-path binding would work
99* many stored non-visual closures in row views
100* `GeometryReader`, preferences, heavy shadows, masks, blurs, or overlays in every row
101* async work started directly from `body`
102* unguarded `.onAppear` pagination triggers inside rows
103
104## Refactor Guidelines
105
106Use the smallest fix that addresses the cause:
107
108* keep identity stable
109* move expensive transformations out of `body`
110* prepare render-ready models outside repeated rendering paths
111* move state closer to the component that owns it
112* split large views into dependency-focused subviews
113* simplify row inputs and repeated structure
114* prefer key-path bindings over custom closure bindings when no transformation is needed
115* use `.equatable()` only when visual equality is clear and cheaper than recomputing
116* use `.task(id:)` when work is tied to a changing input and should cancel/restart automatically
117* consider UIKit only for genuinely hot paths that need lower-level control
118
119Do not use `.equatable()`, memoization, or caching as blanket fixes. Explain the invalidation or rendering issue first.
120
121## Reference Routing
122
123Read references only when the task needs more detail:
124
125* `references/identity-and-state.md` — identity, `.id`, `ForEach`, `@State`, `@StateObject`, `@ObservedObject`, owned observable models
126* `references/observation-and-dependencies.md` — Observation, `ObservableObject`, broad dependencies, environment reads
127* `references/body-cost-and-render-models.md` — sorting, filtering, formatting, derived data, render models
128* `references/lists-pagination-and-rows.md` — `List`, `ScrollView`, `LazyVStack`, rows, pagination, scrolling
129* `references/closures-bindings-and-equatable.md` — closures, bindings, `.equatable()`, action handlers
130* `references/layout-drawing-and-animation.md` — layout, drawing, modifiers, animation hitches
131* `references/async-lifecycle-and-mainactor.md` — `.task`, `.onAppear`, cancellation, `MainActor`
132* `references/profiling-validation.md` — Instruments, `xctrace`, signposts, XCTest, MetricKit
133
134## Response Format
135
136For code reviews, answer with:
137
1381. Main issue
1392. Why it matters in SwiftUI
1403. Risk level
1414. Suggested refactor
1425. Code example when useful
1436. What to measure if confirmation is needed
144
145For profiling requests, answer with:
146
1471. Scenario
1482. Hypothesis
1493. Tool or probe
1504. What to look for
1515. How to compare before and after
152
153Keep the answer concrete. Do not say “SwiftUI is slow.” Explain the specific identity, dependency, rendering, layout, drawing, animation, or lifecycle issue.
154
155## Final Principle
156
157SwiftUI performance improves when code makes change locality obvious.