SwiftUI performance problems rarely come from “slow code”.
They come from misunderstanding how SwiftUI renders views.
That’s why you see:
- views re-rendering “for no reason”
- animations resetting unexpectedly
- lists stuttering
- state seemingly ignored
- random layout glitches
- performance getting worse as the app grows
This post explains how SwiftUI actually renders, what causes invalidations, how identity works, and how to build fast, predictable SwiftUI apps.
🧠 The Core Truth About SwiftUI Performance
SwiftUI is value-based and declarative.
Every time SwiftUI decides something might have changed, it:
- Recomputes
body - Diffs the new view tree
- Decides what to update on screen
Recomputing body is cheap.
Invalidating identity is not.
🔄 1. What Actually Triggers a View Update?
A view updates when:
- a
@Statevalue changes - a
@StateObject/@Observableproperty changes - a
@Bindingchanges - an environment value changes
- a parent view updates
A view does not update when:
- unrelated state changes
- async tasks finish without state mutation
- services update internally without touching state
Understanding what triggers updates eliminates most performance myths.
🆔 2. View Identity: The #1 Performance Killer
SwiftUI tracks views by identity, not by type.
Bad identity example:
ForEach(items) { item in
RowView(item: item)
}
If item.id changes or is unstable → SwiftUI thinks the view is new.
Good identity:
ForEach(items, id: \.id) { item in
RowView(item: item)
}
Rule:
📌 Stable identity = stable performance.
⚠️ 3. The id() Modifier: Powerful and Dangerous
This forces SwiftUI to treat a view as brand new:
Text(title)
.id(UUID()) // ❌ forces recreation every update
Use id() only when you explicitly want:
- animation reset
- scroll reset
- state reset
Never use it casually.
🔁 4. Body Recomputations Are NOT the Problem
This scares people:
var body: some View {
print("render")
...
}
Yes, it prints often.
That’s fine.
SwiftUI is designed to recompute bodies frequently.
Performance problems come from:
- recreating ViewModels
- breaking identity
- triggering expensive layout
- invalidating large subtrees
🧠 5. ViewModel Identity & @StateObject
This is a classic bug:
MyView(viewModel: ViewModel()) // ❌ recreated every render
Correct:
@StateObject var viewModel = ViewModel()
Or inject once from parent.
If your ViewModel is recreated:
- async tasks restart
- caches are lost
- animations reset
- performance tanks
📦 6. Lists & Performance Traps
❌ Common mistakes:
- heavy views inside List
- GeometryReader in rows
- unstable IDs
- large images without caching
- async work inside row bodies
✔ Best practices:
- keep rows lightweight
- precompute data in ViewModel
- paginate aggressively
- avoid layout measurement per row
- cache images
SwiftUI lists are fast if identity and layout are stable.
⚖️ 7. Equatable Views (When to Use Them)
You can reduce updates:
struct RowView: View, Equatable {
let model: Model
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.model.id == rhs.model.id &&
lhs.model.title == rhs.model.title
}
}
SwiftUI skips rendering when values are equal.
Use for:
- complex rows
- dashboards
- frequently updating parents
Don’t overuse — identity comes first.
📐 8. Layout Is Part of Performance
Layout happens every render pass.
Performance killers:
- deep nested stacks
- GeometryReader everywhere
- dynamic size measurement in lists
- constantly changing layout constraints
Better tools:
- intrinsic sizing
- Layout protocol
- ViewThatFits
- caching measured values
Layout inefficiency = render inefficiency.
🔄 9. Environment Updates Are Global Invalidations
Environment values invalidate entire subtrees.
Be careful with:
- large environment objects
- frequently changing global state
- putting fast-changing values in environment
Rule:
📌 Environment = configuration, not volatile state.
🧵 10. Async & Rendering Coordination
Bad pattern:
Task {
data = await load()
}
Good pattern:
@MainActor
func load() async {
loading = true
defer { loading = false }
data = await service.load()
}
Why?
- controlled invalidations
- predictable updates
- no background thread mutations
🧪 11. Diagnosing Performance Issues
Ask these questions:
- Is identity stable?
- Is something being recreated?
- Is layout expensive?
- Is environment invalidating too much?
- Is async mutating state repeatedly?
- Are lists doing heavy work?
Most bugs reveal themselves immediately.
🧠 Mental Performance Model
Think in layers:
State change
↓
View invalidation
↓
Body recompute
↓
Layout
↓
Diff
↓
Render
Control invalidations → control performance.
🚀 Final Thoughts
SwiftUI performance isn’t about micro-optimizations.
It’s about:
- identity
- ownership
- predictable data flow
- controlled invalidation
- clean architecture
Once you master these, SwiftUI apps scale beautifully — even with complex UI.
Top comments (0)