mirror of
https://github.com/Dimillian/Skills.git
synced 2026-03-25 08:55:54 +00:00
improve SwiftUI performance skill
This commit is contained in:
parent
0395637cf4
commit
b638136486
4 changed files with 296 additions and 152 deletions
|
|
@ -5,184 +5,82 @@ description: Audit and improve SwiftUI runtime performance from code review and
|
|||
|
||||
# SwiftUI Performance Audit
|
||||
|
||||
## Overview
|
||||
## Quick start
|
||||
|
||||
Audit SwiftUI view performance end-to-end, from instrumentation and baselining to root-cause analysis and concrete remediation steps.
|
||||
Use this skill to diagnose SwiftUI performance issues from code first, then request profiling evidence when code review alone cannot explain the symptoms.
|
||||
|
||||
## Workflow Decision Tree
|
||||
## Workflow
|
||||
|
||||
- If the user provides code, start with "Code-First Review."
|
||||
- If the user only describes symptoms, ask for minimal code/context, then do "Code-First Review."
|
||||
- If code review is inconclusive, go to "Guide the User to Profile" and ask for a trace or screenshots.
|
||||
1. Classify the symptom: slow rendering, janky scrolling, high CPU, memory growth, hangs, or excessive view updates.
|
||||
2. If code is available, start with a code-first review using `references/code-smells.md`.
|
||||
3. If code is not available, ask for the smallest useful slice: target view, data flow, reproduction steps, and deployment target.
|
||||
4. If code review is inconclusive or runtime evidence is required, guide the user through profiling with `references/profiling-intake.md`.
|
||||
5. Summarize likely causes, evidence, remediation, and validation steps using `references/report-template.md`.
|
||||
|
||||
## 1. Code-First Review
|
||||
## 1. Intake
|
||||
|
||||
Collect:
|
||||
- Target view/feature code.
|
||||
- Data flow: state, environment, observable models.
|
||||
- Symptoms and reproduction steps.
|
||||
- Target view or feature code.
|
||||
- Symptoms and exact reproduction steps.
|
||||
- Data flow: `@State`, `@Binding`, environment dependencies, and observable models.
|
||||
- Whether the issue shows up on device or simulator, and whether it was observed in Debug or Release.
|
||||
|
||||
Ask the user to classify the issue if possible:
|
||||
- CPU spike or battery drain
|
||||
- Janky scrolling or dropped frames
|
||||
- High memory or image pressure
|
||||
- Hangs or unresponsive interactions
|
||||
- Excessive or unexpectedly broad view updates
|
||||
|
||||
For the full profiling intake checklist, read `references/profiling-intake.md`.
|
||||
|
||||
## 2. Code-First Review
|
||||
|
||||
Focus on:
|
||||
- View invalidation storms from broad state changes.
|
||||
- Unstable identity in lists (`id` churn, `UUID()` per render).
|
||||
- Top-level conditional view swapping (`if/else` returning different root branches).
|
||||
- Heavy work in `body` (formatting, sorting, image decoding).
|
||||
- Layout thrash (deep stacks, `GeometryReader`, preference chains).
|
||||
- Large images without downsampling or resizing.
|
||||
- Over-animated hierarchies (implicit animations on large trees).
|
||||
- Invalidation storms from broad observation or environment reads.
|
||||
- Unstable identity in lists and `ForEach`.
|
||||
- Heavy derived work in `body` or view builders.
|
||||
- Layout thrash from complex hierarchies, `GeometryReader`, or preference chains.
|
||||
- Large image decode or resize work on the main thread.
|
||||
- Animation or transition work applied too broadly.
|
||||
|
||||
Use `references/code-smells.md` for the detailed smell catalog and fix guidance.
|
||||
|
||||
Provide:
|
||||
- Likely root causes with code references.
|
||||
- Suggested fixes and refactors.
|
||||
- If needed, a minimal repro or instrumentation suggestion.
|
||||
|
||||
## 2. Guide the User to Profile
|
||||
## 3. Guide the User to Profile
|
||||
|
||||
Explain how to collect data with Instruments:
|
||||
- Use the SwiftUI template in Instruments (Release build).
|
||||
- Reproduce the exact interaction (scroll, navigation, animation).
|
||||
- Capture SwiftUI timeline and Time Profiler.
|
||||
- Export or screenshot the relevant lanes and the call tree.
|
||||
|
||||
Ask for:
|
||||
- Trace export or screenshots of SwiftUI lanes + Time Profiler call tree.
|
||||
If code review does not explain the issue, ask for runtime evidence:
|
||||
- A trace export or screenshots of the SwiftUI timeline and Time Profiler call tree.
|
||||
- Device/OS/build configuration.
|
||||
- The exact interaction being profiled.
|
||||
- Before/after metrics if the user is comparing a change.
|
||||
|
||||
## 3. Analyze and Diagnose
|
||||
Use `references/profiling-intake.md` for the exact checklist and collection steps.
|
||||
|
||||
Prioritize likely SwiftUI culprits:
|
||||
- View invalidation storms from broad state changes.
|
||||
- Unstable identity in lists (`id` churn, `UUID()` per render).
|
||||
- Top-level conditional view swapping (`if/else` returning different root branches).
|
||||
- Heavy work in `body` (formatting, sorting, image decoding).
|
||||
- Layout thrash (deep stacks, `GeometryReader`, preference chains).
|
||||
- Large images without downsampling or resizing.
|
||||
- Over-animated hierarchies (implicit animations on large trees).
|
||||
## 4. Analyze and Diagnose
|
||||
|
||||
Summarize findings with evidence from traces/logs.
|
||||
- Map the evidence to the most likely category: invalidation, identity churn, layout thrash, main-thread work, image cost, or animation cost.
|
||||
- Prioritize problems by impact, not by how easy they are to explain.
|
||||
- Distinguish code-level suspicion from trace-backed evidence.
|
||||
- Call out when profiling is still insufficient and what additional evidence would reduce uncertainty.
|
||||
|
||||
## 4. Remediate
|
||||
## 5. Remediate
|
||||
|
||||
Apply targeted fixes:
|
||||
- Narrow state scope (`@State`/`@Observable` closer to leaf views).
|
||||
- Narrow state scope and reduce broad observation fan-out.
|
||||
- Stabilize identities for `ForEach` and lists.
|
||||
- Move heavy work out of `body` (precompute, cache, `@State`).
|
||||
- Use `equatable()` or value wrappers for expensive subtrees.
|
||||
- Move heavy work out of `body` into derived state updated from inputs, model-layer precomputation, memoized helpers, or background preprocessing. Use `@State` only for view-owned state, not as an ad hoc cache for arbitrary computation.
|
||||
- Use `equatable()` only when equality is cheaper than recomputing the subtree and the inputs are truly value-semantic.
|
||||
- Downsample images before rendering.
|
||||
- Reduce layout complexity or use fixed sizing where possible.
|
||||
|
||||
## Common Code Smells (and Fixes)
|
||||
Use `references/code-smells.md` for examples, Observation-specific fan-out guidance, and remediation patterns.
|
||||
|
||||
Look for these patterns during code review.
|
||||
|
||||
### Expensive formatters in `body`
|
||||
|
||||
```swift
|
||||
var body: some View {
|
||||
let number = NumberFormatter() // slow allocation
|
||||
let measure = MeasurementFormatter() // slow allocation
|
||||
Text(measure.string(from: .init(value: meters, unit: .meters)))
|
||||
}
|
||||
```
|
||||
|
||||
Prefer cached formatters in a model or a dedicated helper:
|
||||
|
||||
```swift
|
||||
final class DistanceFormatter {
|
||||
static let shared = DistanceFormatter()
|
||||
let number = NumberFormatter()
|
||||
let measure = MeasurementFormatter()
|
||||
}
|
||||
```
|
||||
|
||||
### Computed properties that do heavy work
|
||||
|
||||
```swift
|
||||
var filtered: [Item] {
|
||||
items.filter { $0.isEnabled } // runs on every body eval
|
||||
}
|
||||
```
|
||||
|
||||
Prefer precompute or cache on change:
|
||||
|
||||
```swift
|
||||
@State private var filtered: [Item] = []
|
||||
// update filtered when inputs change
|
||||
```
|
||||
|
||||
### Sorting/filtering in `body` or `ForEach`
|
||||
|
||||
```swift
|
||||
List {
|
||||
ForEach(items.sorted(by: sortRule)) { item in
|
||||
Row(item)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Prefer sort once before view updates:
|
||||
|
||||
```swift
|
||||
let sortedItems = items.sorted(by: sortRule)
|
||||
```
|
||||
|
||||
### Inline filtering in `ForEach`
|
||||
|
||||
```swift
|
||||
ForEach(items.filter { $0.isEnabled }) { item in
|
||||
Row(item)
|
||||
}
|
||||
```
|
||||
|
||||
Prefer a prefiltered collection with stable identity.
|
||||
|
||||
### Unstable identity
|
||||
|
||||
```swift
|
||||
ForEach(items, id: \.self) { item in
|
||||
Row(item)
|
||||
}
|
||||
```
|
||||
|
||||
Avoid `id: \.self` for non-stable values; use a stable ID.
|
||||
|
||||
### Top-level conditional view swapping
|
||||
|
||||
```swift
|
||||
var content: some View {
|
||||
if isEditing {
|
||||
editingView
|
||||
} else {
|
||||
readOnlyView
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Prefer one stable base view and localize conditions to sections/modifiers (for example inside `toolbar`, row content, `overlay`, or `disabled`). This reduces root identity churn and helps SwiftUI diffing stay efficient.
|
||||
|
||||
### Image decoding on the main thread
|
||||
|
||||
```swift
|
||||
Image(uiImage: UIImage(data: data)!)
|
||||
```
|
||||
|
||||
Prefer decode/downsample off the main thread and store the result.
|
||||
|
||||
### Broad dependencies in observable models
|
||||
|
||||
```swift
|
||||
@Observable class Model {
|
||||
var items: [Item] = []
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Row(isFavorite: model.items.contains(item))
|
||||
}
|
||||
```
|
||||
|
||||
Prefer granular view models or per-item state to reduce update fan-out.
|
||||
|
||||
## 5. Verify
|
||||
## 6. Verify
|
||||
|
||||
Ask the user to re-run the same capture and compare with baseline metrics.
|
||||
Summarize the delta (CPU, frame drops, memory peak) if provided.
|
||||
|
|
@ -194,9 +92,14 @@ Provide:
|
|||
- Top issues (ordered by impact).
|
||||
- Proposed fixes with estimated effort.
|
||||
|
||||
Use `references/report-template.md` when formatting the final audit.
|
||||
|
||||
## References
|
||||
|
||||
Add Apple documentation and WWDC resources under `references/` as they are supplied by the user.
|
||||
- Profiling intake and collection checklist: `references/profiling-intake.md`
|
||||
- Common code smells and remediation patterns: `references/code-smells.md`
|
||||
- Audit output template: `references/report-template.md`
|
||||
- Add Apple documentation and WWDC resources under `references/` as they are supplied by the user.
|
||||
- Optimizing SwiftUI performance with Instruments: `references/optimizing-swiftui-performance-instruments.md`
|
||||
- Understanding and improving SwiftUI performance: `references/understanding-improving-swiftui-performance.md`
|
||||
- Understanding hangs in your app: `references/understanding-hangs-in-your-app.md`
|
||||
|
|
|
|||
150
swiftui-performance-audit/references/code-smells.md
Normal file
150
swiftui-performance-audit/references/code-smells.md
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
# Common code smells and remediation patterns
|
||||
|
||||
## Intent
|
||||
|
||||
Use this reference during code-first review to map visible SwiftUI patterns to likely runtime costs and safer remediation guidance.
|
||||
|
||||
## High-priority smells
|
||||
|
||||
### Expensive formatters in `body`
|
||||
|
||||
```swift
|
||||
var body: some View {
|
||||
let number = NumberFormatter()
|
||||
let measure = MeasurementFormatter()
|
||||
Text(measure.string(from: .init(value: meters, unit: .meters)))
|
||||
}
|
||||
```
|
||||
|
||||
Prefer cached formatters in a model or dedicated helper:
|
||||
|
||||
```swift
|
||||
final class DistanceFormatter {
|
||||
static let shared = DistanceFormatter()
|
||||
let number = NumberFormatter()
|
||||
let measure = MeasurementFormatter()
|
||||
}
|
||||
```
|
||||
|
||||
### Heavy computed properties
|
||||
|
||||
```swift
|
||||
var filtered: [Item] {
|
||||
items.filter { $0.isEnabled }
|
||||
}
|
||||
```
|
||||
|
||||
Prefer deriving this once per meaningful input change in a model/helper, or store derived view-owned state only when the view truly owns the transformation lifecycle.
|
||||
|
||||
### Sorting or filtering inside `body`
|
||||
|
||||
```swift
|
||||
List {
|
||||
ForEach(items.sorted(by: sortRule)) { item in
|
||||
Row(item)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Prefer sorting before render work begins:
|
||||
|
||||
```swift
|
||||
let sortedItems = items.sorted(by: sortRule)
|
||||
```
|
||||
|
||||
### Inline filtering inside `ForEach`
|
||||
|
||||
```swift
|
||||
ForEach(items.filter { $0.isEnabled }) { item in
|
||||
Row(item)
|
||||
}
|
||||
```
|
||||
|
||||
Prefer a prefiltered collection with stable identity.
|
||||
|
||||
### Unstable identity
|
||||
|
||||
```swift
|
||||
ForEach(items, id: \.self) { item in
|
||||
Row(item)
|
||||
}
|
||||
```
|
||||
|
||||
Avoid `id: \.self` for non-stable values or collections that reorder. Use a stable domain identifier.
|
||||
|
||||
### Top-level conditional view swapping
|
||||
|
||||
```swift
|
||||
var content: some View {
|
||||
if isEditing {
|
||||
editingView
|
||||
} else {
|
||||
readOnlyView
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Prefer one stable base view and localize conditions to sections or modifiers. This reduces root identity churn and makes diffing cheaper.
|
||||
|
||||
### Image decoding on the main thread
|
||||
|
||||
```swift
|
||||
Image(uiImage: UIImage(data: data)!)
|
||||
```
|
||||
|
||||
Prefer decode and downsample work off the main thread, then store the processed image.
|
||||
|
||||
## Observation fan-out
|
||||
|
||||
### Broad `@Observable` reads on iOS 17+
|
||||
|
||||
```swift
|
||||
@Observable final class Model {
|
||||
var items: [Item] = []
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Row(isFavorite: model.items.contains(item))
|
||||
}
|
||||
```
|
||||
|
||||
If many views read the same broad collection or root model, small changes can fan out into wide invalidation. Prefer narrower derived inputs, smaller observable surfaces, or per-item state closer to the leaf views.
|
||||
|
||||
### Broad `ObservableObject` reads on iOS 16 and earlier
|
||||
|
||||
```swift
|
||||
final class Model: ObservableObject {
|
||||
@Published var items: [Item] = []
|
||||
}
|
||||
```
|
||||
|
||||
The same warning applies to legacy observation. Avoid having many descendants observe a large shared object when they only need one derived field.
|
||||
|
||||
## Remediation notes
|
||||
|
||||
### `@State` is not a generic cache
|
||||
|
||||
Use `@State` for view-owned state and derived values that intentionally belong to the view lifecycle. Do not move arbitrary expensive computation into `@State` unless you also define when and why it updates.
|
||||
|
||||
Better alternatives:
|
||||
- precompute in the model or store
|
||||
- update derived state in response to a specific input change
|
||||
- memoize in a dedicated helper
|
||||
- preprocess on a background task before rendering
|
||||
|
||||
### `equatable()` is conditional guidance
|
||||
|
||||
Use `equatable()` only when:
|
||||
- equality is cheaper than recomputing the subtree, and
|
||||
- the view inputs are value-semantic and stable enough for meaningful equality checks
|
||||
|
||||
Do not apply `equatable()` as a blanket fix for all redraws.
|
||||
|
||||
## Triage order
|
||||
|
||||
When multiple smells appear together, prioritize in this order:
|
||||
1. Broad invalidation and observation fan-out
|
||||
2. Unstable identity and list churn
|
||||
3. Main-thread work during render
|
||||
4. Image decode or resize cost
|
||||
5. Layout and animation complexity
|
||||
44
swiftui-performance-audit/references/profiling-intake.md
Normal file
44
swiftui-performance-audit/references/profiling-intake.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Profiling intake and collection checklist
|
||||
|
||||
## Intent
|
||||
|
||||
Use this checklist when code review alone cannot explain the SwiftUI performance issue and you need runtime evidence from the user.
|
||||
|
||||
## Ask for first
|
||||
|
||||
- Exact symptom: CPU spike, dropped frames, memory growth, hangs, or excessive view updates.
|
||||
- Exact interaction: scrolling, typing, initial load, navigation push/pop, animation, sheet presentation, or background refresh.
|
||||
- Target device and OS version.
|
||||
- Whether the issue was reproduced on a real device or only in Simulator.
|
||||
- Build configuration: Debug or Release.
|
||||
- Whether the user already has a baseline or before/after comparison.
|
||||
|
||||
## Default profiling request
|
||||
|
||||
Ask the user to:
|
||||
- Run the app in a Release build when possible.
|
||||
- Use the SwiftUI Instruments template.
|
||||
- Reproduce the exact problematic interaction only long enough to capture the issue.
|
||||
- Capture the SwiftUI timeline and Time Profiler together.
|
||||
- Export the trace or provide screenshots of the key SwiftUI lanes and the Time Profiler call tree.
|
||||
|
||||
## Ask for these artifacts
|
||||
|
||||
- Trace export or screenshots of the relevant SwiftUI lanes
|
||||
- Time Profiler call tree screenshot or export
|
||||
- Device/OS/build configuration
|
||||
- A short note describing what action was happening at the time of the capture
|
||||
- If memory is involved, the memory graph or Allocations data if available
|
||||
|
||||
## When to ask for more
|
||||
|
||||
- Ask for a second capture if the first run mixes multiple interactions.
|
||||
- Ask for a before/after pair if the user has already tried a fix.
|
||||
- Ask for a device capture if the issue only appears in Simulator or if scrolling smoothness matters.
|
||||
|
||||
## Common traps
|
||||
|
||||
- Debug builds can distort SwiftUI timing and allocation behavior.
|
||||
- Simulator traces can miss device-only rendering or memory issues.
|
||||
- Mixed interactions in one capture make attribution harder.
|
||||
- Screenshots without the reproduction note are much harder to interpret.
|
||||
47
swiftui-performance-audit/references/report-template.md
Normal file
47
swiftui-performance-audit/references/report-template.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Audit output template
|
||||
|
||||
## Intent
|
||||
|
||||
Use this structure when reporting SwiftUI performance findings so the user can quickly see the symptom, evidence, likely cause, and next validation step.
|
||||
|
||||
## Template
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
|
||||
[One short paragraph on the most likely bottleneck and whether the conclusion is code-backed or trace-backed.]
|
||||
|
||||
## Findings
|
||||
|
||||
1. [Issue title]
|
||||
- Symptom: [what the user sees]
|
||||
- Likely cause: [root cause]
|
||||
- Evidence: [code reference or profiling evidence]
|
||||
- Fix: [specific change]
|
||||
- Validation: [what to measure after the fix]
|
||||
|
||||
2. [Issue title]
|
||||
- Symptom: ...
|
||||
- Likely cause: ...
|
||||
- Evidence: ...
|
||||
- Fix: ...
|
||||
- Validation: ...
|
||||
|
||||
## Metrics
|
||||
|
||||
| Metric | Before | After | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| CPU | [value] | [value] | [note] |
|
||||
| Frame drops / hitching | [value] | [value] | [note] |
|
||||
| Memory peak | [value] | [value] | [note] |
|
||||
|
||||
## Next step
|
||||
|
||||
[One concrete next action: apply a fix, capture a better trace, or validate on device.]
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Order findings by impact, not by file order.
|
||||
- Say explicitly when a conclusion is still a hypothesis.
|
||||
- If no metrics are available, omit the table and say what should be measured next.
|
||||
Loading…
Reference in a new issue