Audit and improve SwiftUI runtime performance through code review and Instruments guidance...
Audit SwiftUI view performance through code review and provide guidance for Instruments profiling when needed.
SwiftUI views are value types (structs) that describe UI state - they are NOT long-lived objects. Breaking up one view into multiple subviews doesn't hurt performance because views are just declarative descriptions.
Key insight: SwiftUI maintains an efficient data structure behind the scenes. When state changes, new view values are created and SwiftUI determines what actually needs updating. You don't need to compromise code organization for performance.
// โ
GOOD - Splitting into subviews is FREE
var body: some View {
VStack {
HeaderView(title: title) // Separate view = fine
ContentView(items: items) // Separate view = fine
FooterView(action: saveAction) // Separate view = fine
}
}
// SwiftUI only updates the specific subview when its state changes
Look for these common performance issues:
SwiftUI tracks dependencies automatically. Any data a view reads in body becomes a dependency:
@Observable class PetModel {
var name: String = "" // โ If read in body, becomes dependency
var hasAward: Bool = false // โ Only triggers update if actually read
}
struct PetRow: View {
let pet: PetModel
var body: some View {
HStack {
Text(pet.name) // Dependency: name
if pet.hasAward { // Dependency: hasAward
Image(systemName: "star.fill")
}
}
}
}
// When pet.hasAward changes, SwiftUI calls body again automatically
Performance benefit: Only views that actually read changed data get updated.
// BAD - Broad state triggers all views
@Observable class Model {
var items: [Item] = []
}
// GOOD - Granular per-item state
@Observable class ItemModel {
var isFavorite: Bool
}
// BAD - id churn causes full re-render
ForEach(items, id: \.self) { item in Row(item) }
// GOOD - Stable identity
ForEach(items, id: \.id) { item in Row(item) }
body// BAD - Allocation every render
var body: some View {
let formatter = NumberFormatter() // slow
Text(formatter.string(from: value))
}
// GOOD - Cached formatter
static let formatter = NumberFormatter()
var body: some View {
Text(Self.formatter.string(from: value))
}
// BAD - Re-sorts every body eval
ForEach(items.sorted(by: sortRule)) { Row($0) }
// GOOD - Pre-sorted collection
let sortedItems = items.sorted(by: sortRule)
ForEach(sortedItems) { Row($0) }
// BAD - Decodes full resolution on main thread
Image(uiImage: UIImage(data: data)!)
// GOOD - Downsample off main thread first
| Pattern | Problem | Fix |
|---|---|---|
NumberFormatter() in body |
Allocation per render | Static cached formatter |
.filter { } in ForEach |
Recomputes every render | Pre-filter, cache result |
id: \.self on non-stable values |
Identity churn | Use stable ID property |
UUID() per render |
New identity every time | Store ID in model |
GeometryReader deep in tree |
Layout thrash | Move up or use fixed sizes |
if condition { View } in ForEach |
Variable view count forces full build | Use opacity(0) or pre-filter |
AnyView in List rows |
Hides identity and view count | Use @ViewBuilder or concrete types |
When code review is inconclusive, guide user to profile:
Ask user for:
@State/@Observable closer to leaf views)ForEach and listsbody (precompute, cache, @State)equatable() for expensive subtreesFor detailed WWDC guidance:
references/demystify-swiftui-performance-wwdc23.mdreferences/optimizing-swiftui-performance-instruments.mdreferences/understanding-improving-swiftui-performance.mdNavigation: This skill provides SwiftUI performance audit patterns. For general iOS development, see ios-dev-guidelines.
Attribution: Patterns adapted from Dimillian/Skills repository. WWDC24 insights from "SwiftUI Essentials" session.