Install
$ agentstack add skill-koshkinvv-ios-agent-skills-ios-performance ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
iOS Performance Optimization Skill
Core Rules
- Use
[weak self]in escaping closures by default — use[unowned self]only when the closure's lifetime is strictly shorter than the captured object (e.g., parent owns child, child's closure references parent). - Non-escaping closures do NOT need
[weak self]—map,filter,forEach,reduce,compactMap,sorted(by:)are non-escaping. The closure executes synchronously and releases captures immediately. - Delegates must be
weak var— the delegate protocol must conform toAnyObject(or be marked@objc). Strong delegates create retain cycles between owner and delegate. - Use
@ObservableoverObservableObject(iOS 17+) —@Observabletracks property access per-view, so only views reading a changed property re-evaluate.ObservableObjectwith@Publishedinvalidates ALL observing views on ANY published property change. - Break SwiftUI views into small subviews — each subview localizes its state dependency, so changes only re-evaluate the subview, not the entire parent body.
- Use
LazyVStack/LazyHStackinsideScrollViewfor large collections —VStackevaluates ALL children upfront. UseListfor very large datasets (10,000+) since it reuses cells. - Use
.taskmodifier instead of.onAppear+Task {}—.taskautomatically cancels when the view disappears. Use.task(id:)to restart when a dependency changes. - Profile with Instruments BEFORE optimizing — measure first, optimize second. Never guess where the bottleneck is.
- Never block the main thread — all file I/O, network calls, JSON decoding of large payloads, image processing, and database queries must run off the main thread.
- Reuse expensive objects —
URLSession,DateFormatter,JSONDecoder,NumberFormatter,NSRegularExpressionare expensive to create. Use shared instances or caches.
Performance Targets
| Metric | Target | Critical Threshold | |--------|--------|--------------------| | Cold launch | 2s triggers watchdog kill | | Warm launch | 1s feels broken | | Frame rate | 60 FPS (120 on ProMotion) | 33ms = visible dropped frame | | Memory (typical) | 500MB risks jetsam on older devices | | Memory (spike) | 5% drains battery | | CPU active task | 1s needs loading indicator | | App size (download) | 200MB requires Wi-Fi | | Disk writes | Void = { [unowned self] in self.doSomething() // Parent always outlives its own lazy property } }
// NOT NEEDED: Non-escaping closure — no capture cycle possible let names = users.map { $0.name } // map is non-escaping let adults = users.filter { $0.age >= 18 } // filter is non-escaping items.forEach { print($0) } // forEach is non-escaping
// CORRECT: Weak delegate protocol DataServiceDelegate: AnyObject { func didUpdate(_ data: Data) }
class DataService { weak var delegate: DataServiceDelegate? }
## SwiftUI Performance Quick Reference
```swift
// BAD: One large view — any state change re-evaluates entire body
struct ProfileView: View {
@State private var name = ""
@State private var bio = ""
@State private var avatarURL: URL?
@State private var posts: [Post] = []
var body: some View {
ScrollView {
avatarSection // Change to name re-evaluates avatar too
bioSection
postsSection // All 500 post cells re-evaluated
}
}
}
// GOOD: Extracted subviews — state changes localized
struct ProfileView: View {
var body: some View {
ScrollView {
AvatarSection() // Only re-evaluates when avatar changes
BioSection() // Only re-evaluates when bio changes
PostsSection() // Only re-evaluates when posts change
}
}
}
// GOOD: LazyVStack for scrollable content
struct PostsSection: View {
let posts: [Post]
var body: some View {
LazyVStack { // Only creates visible cells + prefetch buffer
ForEach(posts) { post in
PostRow(post: post)
}
}
}
}
// GOOD: .task with auto-cancellation
struct UserDetailView: View {
let userID: String
@State private var user: User?
var body: some View {
content
.task(id: userID) { // Cancels & restarts if userID changes
user = try? await api.fetchUser(userID)
}
}
}
@Observable vs ObservableObject
// OLD (iOS 14+): ObservableObject — ALL views re-evaluate on ANY change
class UserViewModel: ObservableObject {
@Published var name = "" // Change triggers ALL observers
@Published var email = "" // Change triggers ALL observers
@Published var avatarURL: URL? // Change triggers ALL observers
}
// NEW (iOS 17+): @Observable — only views reading changed property re-evaluate
@Observable
class UserViewModel {
var name = "" // Only views reading `name` re-evaluate
var email = "" // Only views reading `email` re-evaluate
var avatarURL: URL? // Only views reading `avatarURL` re-evaluate
}
Instruments Workflow
Step 1: Profile, Don't Debug
Always profile on a real device (not Simulator). Use Release configuration for accurate measurements.
Step 2: Choose the Right Instrument
- Time Profiler — CPU bottlenecks, main thread blocking
- Allocations — memory growth, leaks, allocation hotspots
- Leaks — automatic retain cycle detection (periodic snapshots)
- Memory Graph Debugger (Xcode, not Instruments) — visual reference chains
- Core Animation — rendering performance, blended layers, offscreen rendering
- Energy Log — battery drain causes (CPU, network, GPS, Bluetooth)
- Network — HTTP request/response analysis
- App Launch — cold/warm launch breakdown
- SwiftUI (Xcode 16+) — view body evaluations, cause & effect graph
Step 3: Time Profiler Settings (Always Set These)
- Invert Call Tree — shows heaviest leaf functions first
- Separate by Thread — isolates main thread work
- Hide System Libraries — focuses on your code
- Separate by State — shows running vs blocked time
Step 4: Record, Reproduce, Analyze
- Record for 10-30 seconds covering the problematic interaction
- Select the time range of interest
- Look at the heaviest stack traces
- Focus on main thread first (Thread 1)
App Launch Optimization Checklist
Pre-main Phase (<200ms target)
- [ ] Max 6 non-system dynamic frameworks (each adds ~10-20ms)
- [ ] No
+loadmethods in ObjC code (move to+initializeor lazy init) - [ ] Minimize static initializers (C++ globals,
__attribute__((constructor))) - [ ] Use static linking where possible (SPM default)
Post-main Phase (<200ms target)
- [ ] Defer non-essential initialization (analytics, logging, feature flags)
- [ ] Use
lazy varfor expensive properties - [ ] Load first screen data from cache, then refresh from network
- [ ] Avoid synchronous network calls at launch
- [ ] Minimize work in
application(_:didFinishLaunchingWithOptions:) - [ ] Use
Scenephase detection instead of heavy AppDelegate setup
Common Anti-Patterns
1. Main Thread Blocking
// BAD: Synchronous file read on main thread
let data = try! Data(contentsOf: largeFileURL)
// GOOD: Async file read
let data = try await Task.detached {
try Data(contentsOf: largeFileURL)
}.value
2. Excessive Allocations in Loops
// BAD: Creates new DateFormatter per iteration (expensive!)
for event in events {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
labels.append(formatter.string(from: event.date))
}
// GOOD: Reuse formatter
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
for event in events {
labels.append(formatter.string(from: event.date))
}
3. VStack for Large Collections
// BAD: Creates ALL 10,000 views upfront
ScrollView {
VStack {
ForEach(items) { item in // 10,000 items = 10,000 views in memory
ItemRow(item: item)
}
}
}
// GOOD: Only creates visible views
ScrollView {
LazyVStack {
ForEach(items) { item in // ~20 views in memory at a time
ItemRow(item: item)
}
}
}
4. Retaining Self in Timers
// BAD: Timer retains self, self retains timer → cycle
class PollingService {
var timer: Timer?
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
self.poll() // Strong capture → retain cycle
}
}
}
// GOOD: Weak capture
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in
self?.poll()
}
}
5. Not Cancelling Tasks
// BAD: Task keeps running after view disappears
.onAppear {
Task {
while !Task.isCancelled {
await refresh()
try await Task.sleep(for: .seconds(30))
}
}
}
// GOOD: .task auto-cancels on disappear
.task {
while !Task.isCancelled {
await refresh()
try? await Task.sleep(for: .seconds(30))
}
}
Reference Files
For deep dives, see:
references/memory.md— ARC, retain cycles, value vs reference types, copy-on-write, debuggingreferences/swiftui-perf.md— View identity, @Observable, lazy containers, images, .taskreferences/instruments.md— Time Profiler, Allocations, Leaks, Energy, Core Animation, SwiftUI instrumentreferences/optimization.md— App launch, network, battery, build performance, anti-patterns
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: koshkinvv
- Source: koshkinvv/ios-agent-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.