SwiftUI can feel mysterious when it comes to rendering.
You change one value…
Suddenly:
- views re-render
- animations restart
- layout recalculates
- performance drops
- things update “too often”
This happens because SwiftUI has a rendering pipeline — and most developers never learn how it works.
This post explains exactly how SwiftUI renders your UI, step by step, using the modern mental model.
Once you understand this pipeline, SwiftUI stops feeling random.
🧠 The Big Picture
Every SwiftUI update goes through the same pipeline:
State Change
↓
View Invalidated
↓
body Recomputed
↓
Layout Pass
↓
Diffing
↓
Rendering
Nothing skips this.
Nothing is magic.
Performance problems happen when too much work happens in one of these stages.
🔥 1. State Change (The Only Entry Point)
Everything starts with state.
Examples:
- @State changes
- @StateObject / @observable property changes
- @Binding updates
- Environment value changes
If no state changes → nothing re-renders.
This is why SwiftUI apps can be extremely efficient.
⚠️ Common Mistake
Developers often think:
“body recomputes too much”
That’s not the problem.
body recomputation is cheap.
Invalidation + layout + diffing is what costs time.
🧱 2. View Invalidation
When state changes, SwiftUI:
- marks affected views as dirty
- schedules them for update
Important:
- only affected subtrees invalidate
- not the entire app
Bad architecture (global state everywhere) = massive invalidation.
Good architecture (scoped state) = minimal invalidation.
🔁 3. body Recomposition (Cheap)
SwiftUI re-executes:
var body: some View { ... }
This is:
- fast
- expected
- frequent
SwiftUI compares values, not objects.
Recomposing views is cheap.
Recreating identity is not.
🧠 Key Insight
SwiftUI is optimized for frequent body recomputation, not frequent identity changes.
📐 4. Layout Pass
After body recomputation, SwiftUI performs layout:
- Parent proposes size
- Child chooses size
- Parent positions child
This happens:
- after every invalidation
- during animations
- during size changes
- during rotations
- during list updates
Layout becomes expensive when:
- GeometryReader is overused
- layouts depend on dynamic measurements
- views constantly change size
🧨 Layout Performance Killers
- GeometryReader inside lists
- deeply nested stacks
- dynamic size measurement per frame
- view size depending on async data repeatedly
🔍 5. Diffing (The Critical Step)
SwiftUI compares:
- previous view tree
- new view tree
Using:
- view type
- position
- identity
(id)
If identity matches:
- state is preserved
- view is updated in place
If identity changes:
- state is destroyed
- view recreated
- animations reset
- tasks restart
Diffing is where most “bugs” come from.
🆔 Identity Is Everything
This is fast:
ForEach(items, id: .id) { item in
Row(item)
}
This is expensive and buggy:
ForEach(items) { item in
Row(item)
.id(UUID())
}
You just forced a full teardown every update.
🎨 6. Rendering (GPU Stage)
After diffing:
- SwiftUI issues drawing commands
- GPU renders the final pixels
Rendering is usually fast unless you use:
- heavy blurs
- layered materials
- complex masks
- offscreen rendering
- too many shadows
Rendering problems show up as:
- dropped frames
- janky animations
- scrolling stutter
🧵 7. Animations in the Pipeline
Animations affect every stage:
- state changes → animated
- layout interpolated
- rendering interpolated
Important:
- animations don’t skip layout
- animations don’t skip diffing
- animations amplify inefficiencies
This is why poor architecture feels much worse when animated.
⚙️ 8. Async & Rendering Coordination
Bad pattern:
Task {
data = await load()
}
Better:
@MainActor
func load() async {
isLoading = true
defer { isLoading = false }
data = await service.load()
}
Why?
- predictable invalidation
- single render cycle
- avoids cascading updates
Async should batch state changes, not drip-feed them.
🧪 9. Why Lists Are Special
Lists:
- reuse views aggressively
- rely heavily on identity
- trigger frequent layout passes
- render offscreen rows
Performance depends on:
- stable IDs
- lightweight rows
- minimal layout work
- cached data
Lists amplify every mistake in the rendering pipeline.
🧠 Mental Debugging Checklist
When something feels slow, ask:
- What state changed?
- How much did it invalidate
- Did identity change?
- Did layout get expensive?
- Did animation amplify the cost?
- Did async trigger multiple updates?
You can almost always pinpoint the issue.
🚀 Final Mental Model
Think in layers, not code:
State
↓
Invalidation
↓
Body
↓
Layout
↓
Diffing
↓
Rendering
Control:
- state scope
- identity stability
- layout complexity
…and SwiftUI becomes fast, predictable, and scalable.
🏁 Final Thoughts
SwiftUI performance isn’t about tricks.
It’s about respecting the rendering pipeline.
Once you understand:
- where work happens
- why identity matters
- how layout impacts performance
You stop fighting SwiftUI — and start working with it.
