Ios performance profiler
Skill Desquared/agents-rules-skills/skills/ios-performance-profiler
Identifies potential performance bottlenecks in SwiftUI code — expensive view body computation, unnecessary redraws, identity instability, concurrency issues, and memory problems. Use for lists, animations, complex layouts, remote data, or when the user asks about performance.From its SKILL.md
npx -y skills add Desquared/agents-rules-skills --skill ios-performance-profilerAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
5.8 KB, as published. Nobody here has run it
Performance Profiler
View Identity & Diffing
- Stable, persistent identity (
Identifiablewith stableid, avoid random UUIDs in rows) - Prefer value types + structural identity over class reference identity when possible
- Avoid
.id()unless absolutely necessary (use stable keys instead)
Body Computation & Redraws
- No heavy work (network, decoding, sorting, filtering) inside
body - Expensive derived state computed once and stored (
.onAppear, ViewModel, or memoized computed property) -
@State,@Bindable,@Observableproperties are narrowly scoped - Child views extracted when parent state changes frequently
- Prefer
@Observable(Observation framework) over@ObservableObject/@Publishedfor new code — finer-grained invalidation, less boilerplate
Lists & Large Collections
- Use
List/LazyVStack/LazyHGridinstead ofScrollView + ForEach + VStack - Rows are lightweight; avoid
@StateObject/@ObservedObjectinside row bodies - Use stable
Identifiableconformance or explicitid:key path
Concurrency & Main Actor (Swift 6 era)
-
@MainActorused appropriately on view-bound types (avoid overuse → thread hops) - Nonisolated properties / computed vars when safe
- Actors used for model / service layers; avoid
@MainActoron pure data models - No data races when using
@Observabletypes across concurrency domains - Prefer
.taskover.onAppearfor async work (automatic cancellation on view disappear, structured concurrency)
Environment & State Propagation
- Avoid injecting large
@Observableobjects via@Environmentwhen only a small slice of state is needed — extract a child view that reads only what it needs - Minimize over-subscription from
EnvironmentObject— split into focused, smaller observable types if necessary
Memory & Retain Cycles
-
[weak self]in async / escaping closures that captureself -
@StateObjectfor view-owned view models (not inListrows) - Remote images use proper caching (
AsyncImagewith cache, orKingfisher/Nuke) - Downscale images to display size before rendering (
.resizable()+.frame()alone doesn't reduce memory) - Use
preparingThumbnail(of:)for large images - Avoid long-lived strong references in
@Observable/@ObservableObject
Layout & Geometry
- Minimize
GeometryReaderusage inside scroll views (causes layout thrashing) - Avoid
PreferenceKeyupdates that trigger parent re-layouts in tight loops - Use
drawingGroup()for complex vector paths / heavy Canvas content
Animations & Transitions
- Scope animations narrowly (
.animation(…, value: …)) instead of global.animation() - Use
.matchedGeometryEffectonly when necessary (expensive) - Test with reduced motion enabled
Navigation & Sheets
- Use lazy destination resolution in
NavigationStack(avoid pre-building destination views) - Ensure
.sheet/.fullScreenCovercontent isn't computed until presentation - Avoid deep view hierarchies in destinations — flatten or split into smaller views
Quick Wins
| Issue | Fix / Pattern | Severity |
|---|---|---|
| Parent redraws child unnecessarily | Extract stable child view / use Equatable conformance | 🟡 |
Heavy logic in body | Move to ViewModel / .task / computed property | 🔴 |
Unstable ForEach IDs | Use stable id: \.self or persistent model ID | 🔴 |
| Retain cycle | \[weak self\] in closures | 🔴 |
@StateObject recreated in List row | Move ownership to parent / use @Observable value type | 🔴 |
| Broad / unnecessary animations | .animation(nil) or value-specific .animation(…, value:) | 🟡 |
| Frequent main-actor hops | Use nonisolated properties, actors for model layer | 🟡 |
GeometryReader in scroll views | Move geometry reads outside scroll or cache values | 🟡 |
| Large images not downscaled | preparingThumbnail(of:) or resize before display | 🟡 |
Over-subscribed @Environment | Extract child view reading only needed state | 🟡 |
| Eager sheet / destination init | Lazy destination resolution in NavigationStack | 🟡 |
Still using @ObservableObject | Migrate to @Observable (Observation framework) | 🟢 |
| Complex vector / Canvas paths | Use drawingGroup() for off-screen rendering | 🟢 |
Debug Helpers (Xcode 16+)
// In any View body — logs when/why view redraws
let _ = Self._printChanges()
// os_signpost for custom performance intervals
import os.signpost
let log = OSLog(subsystem: "com.app.performance", category: "ViewLoading")
let signpostID = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "LoadData", signpostID: signpostID)
// ... work ...
os_signpost(.end, log: log, name: "LoadData", signpostID: signpostID)
Instruments & Tools
- SwiftUI template + Animation instrument in Instruments
- SwiftUI View Body instrument (Xcode 16+) — tracks body evaluations per view
- SwiftUI Performance HUD —
⌥⌘Pin Simulator - MetricKit /
MXMetricManager— production performance & diagnostic data - Memory Graph Debugger — catch retain cycles and leaks at runtime
Severity Legend
🔴 Critical — Visible lag, dropped frames, memory growth, crashes
🟡 Moderate — Noticeable jank on low-end devices or large datasets
🟢 Minor — Optimization opportunity / future-proofs for larger scale