agentsclimarketplace

Swiftui performance audit

Skill patrickserrano/lacquer/profiles/ios/skills/swiftui-performance-audit

Go CLI + profile templates that standardize how Claude Code works across every project

Install
npx -y skills add patrickserrano/lacquer --skill swiftui-performance-audit

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 2 stars2 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.

What its author says it does

Copied from the file, not written here

Audit and improve SwiftUI runtime performance. Use for requests to diagnose slow rendering, janky scrolling, high CPU/memory usage, excessive view updates, or layout thrash in SwiftUI apps.

SKILL.md

7.4 KB, ~1.8k tokens by cl100k_base, as published. Nobody here has run it

SwiftUI Performance Audit

Overview

Audit SwiftUI view performance from instrumentation and baselining to root-cause analysis and concrete remediation steps.

Workflow Decision Tree

  1. If user provides code: Start with Code-First Review
  2. If user only describes symptoms: Ask for minimal code/context, then Code-First Review
  3. If code review is inconclusive: Guide user to profile with Instruments

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
  • Over-animated hierarchies (implicit animations on large trees)
  • Non-lazy containers (VStack/HStack) holding large collections instead of LazyVStack/LazyHStack
  • Async work in .task without relying on its automatic cancellation when the view disappears
  • Closures that may run off the main thread (Shape.path(in:), visualEffect, Layout protocol methods, onGeometryChange) touching @MainActor state directly instead of capturing values

Provide

  • Likely root causes with code references
  • Suggested fixes and refactors
  • Minimal repro or instrumentation suggestion if needed

2. Guide User to Profile

Before reaching for Instruments, a cheaper first step: ask the user to add Self._printChanges() (prints to stdout) or Self._logChanges() (iOS 17+, logs to the com.apple.SwiftUI subsystem under "Changed Body Properties") as the first line of the suspect view's body. Both print @self when the view value itself changed and @identity when the view's persistent data was recycled -- this often narrows down the offending state before a trace is needed. Remove these calls before shipping.

If code review is inconclusive, explain how to collect data:

  1. Use SwiftUI template in Instruments (Release build)
  2. Reproduce the exact interaction (scroll, navigation, animation)
  3. Capture SwiftUI timeline and Time Profiler
  4. Export or screenshot relevant lanes and call tree

Ask for:

  • Trace export or screenshots
  • Device/OS/build configuration

3. Common Code Smells (and Fixes)

Expensive formatters in body

Bad:

var body: some View {
    let formatter = NumberFormatter()  // Slow allocation every render
    Text(formatter.string(from: value))
}

Good:

final class Formatters {
    static let number = NumberFormatter()
}

var body: some View {
    Text(Formatters.number.string(from: value))
}

Computed properties with heavy work

Bad:

var filtered: [Item] {
    items.filter { $0.isEnabled }  // Runs every body eval
}

Good:

@State private var filtered: [Item] = []

.onChange(of: items) {
    filtered = items.filter { $0.isEnabled }
}

Sorting/filtering in ForEach

Bad:

ForEach(items.sorted(by: sortRule)) { item in
    Row(item)
}

Good:

let sortedItems = items.sorted(by: sortRule)  // Compute once
ForEach(sortedItems) { item in
    Row(item)
}

Unstable identity

Bad:

ForEach(items, id: \.self) { item in  // \.self may not be stable
    Row(item)
}

Good:

ForEach(items, id: \.stableID) { item in
    Row(item)
}

Image decoding on main thread

Bad:

Image(uiImage: UIImage(data: data)!)

Good:

// Decode/downsample off main thread, cache the result
@State private var image: UIImage?

.task {
    image = await ImageLoader.load(data: data, targetSize: size)
}

Broad dependencies in observable models

Bad:

@Observable class Model {
    var items: [Item] = []
}

var body: some View {
    Row(isFavorite: model.items.contains(item))  // Entire array dependency
}

Good:

// Granular view models or per-item state to reduce update fan-out

Non-POD views in hot paths

A view is POD (Plain Old Data) when it only holds simple value types and no property wrappers -- SwiftUI diffs it with fast memcmp instead of reflection. Wrap an expensive non-POD view in a POD parent so the fast comparison gates the expensive one:

Bad:

struct ExpensiveView: View {
    let value: Int
    @State private var item: Item?  // property wrapper makes this non-POD

    var body: some View {
        // expensive rendering, re-diffed via reflection every time
    }
}

Good:

// POD wrapper -- fast memcmp diffing gates the expensive internal view
struct ExpensiveView: View {
    let value: Int

    var body: some View {
        ExpensiveViewInternal(value: value)
    }
}

private struct ExpensiveViewInternal: View {
    let value: Int
    @State private var item: Item?

    var body: some View {
        // expensive rendering, only diffed when `value` changes
    }
}

Off-main-thread closures touching @MainActor state

SwiftUI may invoke Shape.path(in:), the visualEffect closure, Layout protocol methods, and the onGeometryChange transform closure on a background thread. They must be Sendable and should capture needed values instead of reading @MainActor-isolated state directly:

Bad:

.visualEffect { content, geometry in
    content.blur(radius: self.pulse ? 5 : 0)  // compiler error: @MainActor isolated
}

Good:

.visualEffect { [pulse] content, geometry in
    content.blur(radius: pulse ? 5 : 0)
}

4. Remediation Strategies

IssueFix
Broad state changesNarrow scope with @State/@Observable closer to leaves
Unstable identitiesUse stable, unique IDs for ForEach
Heavy work in bodyPrecompute, cache, move to @State
Expensive subtreesUse equatable() or value wrappers, or a POD wrapper view
Large imagesDownsample before rendering
Layout complexityReduce nesting, use fixed sizing where possible
Large collections in eager containersUse LazyVStack/LazyHStack/LazyVGrid/LazyHGrid
Unnecessary derived stateCompute via a var instead of storing a second @State that must be kept in sync
Off-main-thread closures reading @MainActor stateCapture values in the closure's capture list instead

5. Verify

Ask user to re-run same capture and compare with baseline:

  • CPU usage
  • Frame drops
  • Memory peak

Output Format

Provide:

  1. Metrics table (before/after if available)
  2. Top issues (ordered by impact)
  3. Proposed fixes with estimated effort

Profiling Commands

# Build for profiling
xcodebuild -scheme MyApp -configuration Release -destination 'platform=iOS Simulator,name=iPhone 15 Pro' build

# Open in Instruments
open -a Instruments

Instruments Checklist

  • Use Release build (not Debug)
  • Select SwiftUI template
  • Reproduce exact problematic interaction
  • Look at SwiftUI timeline for body evaluations
  • Check Time Profiler for hot spots
  • Note frame rate drops in Animation timeline

Source: split from AvdLee/SwiftUI-Agent-Skill's reference material.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,984. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.