Skill: steipete/swiftui-performance-audit (version 1.0.0)

--- name: swiftui-performance-audit description: Audit and improve SwiftUI runtime performance from code review and architecture. Use for requests to diagnose slow rendering, janky scrolling, high CPU/memory usage, excessive view updates, or layout thrash in SwiftUI apps, and to provide guidance for user-run Instruments profiling when code review alone is insufficient. --- # SwiftUI Performance Audit _Attribution: copied from @Dimillian’s `Dimillian/Skills` (2025-12-31)._ ## Overview Audit SwiftUI view performance end-to-end, from instrumentation and baselining to root-cause analysis and concrete remediation steps. ## Workflow Decision Tree - 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. Code-First Review Collect: - Target view/feature code. - Data flow: state, environment, observable models. - Symptoms and reproduction steps. Focus on: - View invalidation storms from broad state changes. - Unstable identity in lists (`id` churn, `UUID()` per render). - 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). 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 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. - Device/OS/build configuration. ## 3. Analyze and Diagnose Prioritize likely SwiftUI culprits: - View invalidation storms from broad state changes. - Unstable identity in lists (`id` churn, `UUID()` per render). - 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). Summarize findings with evidence from traces/logs. ## 4. Remediate Apply targeted fixes: - Narrow state scope (`@State`/`@Observable` closer to leaf views). - Stabilize identities for `ForEach` and lists. - Move heavy work out of `body` (precompute, cache, `@State`). - Use `equatable()` or value wrappers for expensive subtrees. - Downsample images before rendering. - Reduce layout complexity or use fixed sizing where possible. ## Common Code Smells (and Fixes) 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. ### 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 Ask the user to re-run the same capture and compare with baseline metrics. Summarize the delta (CPU, frame drops, memory peak) if provided. ## Outputs Provide: - A short metrics table (before/after if available). - Top issues (ordered by impact). - Proposed fixes with estimated effort. ## References 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` - Demystify SwiftUI performance (WWDC23): `references/demystify-swiftui-performance-wwdc23.md`

path: references/demystify-swiftui-performance-wwdc23.md
content: # Demystify SwiftUI Performance (WWDC23) (Summary)

Context: WWDC23 session on building a mental model for SwiftUI performance and triaging hangs/hitches.

## Performance loop

- Measure -> Identify -> Optimize -> Re-measure.
- Focus on concrete symptoms (slow navigation, broken animations, spinning cursor).

## Dependencies and updates

- Views form a dependency graph; dynamic properties are a frequent source of updates.
- Use `Self._printChanges()` in debug only to inspect extra dependencies.
- Eliminate unnecessary dependencies by extracting views or narrowing state.
- Consider `@Observable` for more granular property tracking.

## Common causes of slow updates

- Expensive view bodies (string interpolation, filtering, formatting).
- Dynamic property instantiation and state initialization in `body`.
- Slow identity resolution in lists/tables.
- Hidden work: bundle lookups, heap allocations, repeated string construction.

## Avoid slow initialization in view bodies

- Don’t create heavy models synchronously in view bodies.
- Use `.task` to fetch async data and keep `init` lightweight.

## Lists and tables identity rules

- Stable identity is critical for performance and animation.
- Ensure a constant number of views per element in `ForEach`.
- Avoid inline filtering in `ForEach`; pre-filter and cache collections.
- Avoid `AnyView` in list rows; it hides identity and increases cost.
- Flatten nested `ForEach` when possible to reduce overhead.

## Table specifics

- `TableRow` resolves to a single row; row count must be constant.
- Prefer the streamlined `Table` initializer to enforce constant rows.
- Use explicit IDs for back deployment when needed.

## Debugging aids

- Use Instruments for hangs and hitches.
- Use `_printChanges` to validate dependency assumptions during debug.


path: references/optimizing-swiftui-performance-instruments.md
content: # Optimizing SwiftUI Performance with Instruments (Summary)

Context: WWDC session introducing the next-generation SwiftUI Instrument in Instruments 26 and how to diagnose SwiftUI-specific bottlenecks.

## Key takeaways

- Profile SwiftUI issues with the SwiftUI template (SwiftUI instrument + Time Profiler + Hangs/Hitches).
- Long view body updates are a common bottleneck; use "Long View Body Updates" to identify slow bodies.
- Set inspection range on a long update and correlate with Time Profiler to find expensive frames.
- Keep work out of `body`: move formatting, sorting, image decoding, and other expensive work into cached or precomputed paths.
- Use Cause & Effect Graph to diagnose *why* updates occur; SwiftUI is declarative, so backtraces are often unhelpful.
- Avoid broad dependencies that trigger many updates (e.g., `@Observable` arrays or global environment reads).
- Prefer granular view models and scoped state so only the affected view updates.
- Environment values update checks still cost time; avoid placing fast-changing values (timers, geometry) in environment.
- Profile early and often during feature development to catch regressions.

## Suggested workflow (condensed)

1. Record a trace in Release mode using the SwiftUI template.
2. Inspect "Long View Body Updates" and "Other Long Updates."
3. Zoom into a long update, then inspect Time Profiler for hot frames.
4. Fix slow body work by moving heavy logic into precomputed/cache paths.
5. Use Cause & Effect Graph to identify unintended update fan-out.
6. Re-record and compare the update counts and hitch frequency.

## Example patterns from the session

- Caching formatted distance strings in a location manager instead of computing in `body`.
- Replacing a dependency on a global favorites array with per-item view models to reduce update fan-out.


path: references/understanding-hangs-in-your-app.md
content: # Understanding Hangs in Your App (Summary)

Context: Apple guidance on identifying hangs caused by long-running main-thread work and understanding the main run loop.

## Key concepts

- A hang is a noticeable delay in a discrete interaction (typically >100 ms).
- Hangs almost always come from long-running work on the main thread.
- The main run loop processes UI events, timers, and main-queue work sequentially.

## Main-thread work stages

- Event delivery to the correct view/handler.
- Your code: state updates, data fetch, UI changes.
- Core Animation commit to the render server.

## Why the main run loop matters

- Only the main thread can update UI safely.
- The run loop is the foundation that executes main-queue work.
- If the run loop is busy, it can’t handle new events; this causes hangs.

## Diagnosing hangs

- Observe the main run loop’s busy periods: healthy loops sleep most of the time.
- Hang detection typically flags busy periods >250 ms.
- The Hangs instrument can be configured to lower thresholds.

## Practical takeaways

- Keep main-thread work short; offload heavy work from event handlers.
- Avoid long-running tasks on the main dispatch queue or main actor.
- Use run loop behavior as a proxy for user-perceived responsiveness.


path: references/understanding-improving-swiftui-performance.md
content: # Understanding and Improving SwiftUI Performance (Summary)

Context: Apple guidance on diagnosing SwiftUI performance with Instruments and applying design patterns to reduce long or frequent updates.

## Core concepts

- SwiftUI is declarative; view updates are driven by state, environment, and observable data dependencies.
- View bodies must compute quickly to meet frame deadlines; slow or frequent updates lead to hitches.
- Instruments is the primary tool to find long-running updates and excessive update frequency.

## Instruments workflow

1. Profile via Product > Profile.
2. Choose the SwiftUI template and record.
3. Exercise the target interaction.
4. Stop recording and inspect the SwiftUI track + Time Profiler.

## SwiftUI timeline lanes

- Update Groups: overview of time SwiftUI spends calculating updates.
- Long View Body Updates: orange >500us, red >1000us.
- Long Platform View Updates: AppKit/UIKit hosting in SwiftUI.
- Other Long Updates: geometry/text/layout and other SwiftUI work.
- Hitches: frame misses where UI wasn’t ready in time.

## Diagnose long view body updates

- Expand the SwiftUI track; inspect module-specific subtracks.
- Set Inspection Range and correlate with Time Profiler.
- Use call tree or flame graph to identify expensive frames.
- Repeat the update to gather enough samples for analysis.
- Filter to a specific update (Show Calls Made by `MySwiftUIView.body`).

## Diagnose frequent updates

- Use Update Groups to find long active groups without long updates.
- Set inspection range on the group and analyze update counts.
- Use Cause graph ("Show Causes") to see what triggers updates.
- Compare causes with expected data flow; prioritize the highest-frequency causes.

## Remediation patterns

- Move expensive work out of `body` and cache results.
- Use `Observable()` macro to scope dependencies to properties actually read.
- Avoid broad dependencies that fan out updates to many views.
- Reduce layout churn; isolate state-dependent subtrees from layout readers.
- Avoid storing closures that capture parent state; precompute child views.
- Gate frequent updates (e.g., geometry changes) by thresholds.

## Verification

- Re-record after changes to confirm reduced update counts and fewer hitches.


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)