SwiftUI is fast — but only if you use it correctly.
Over time, you’ll run into:
- choppy scroll performance
- laggy animations
- slow lists
- views refreshing too often
- unnecessary recomputations
- async work blocking UI
- memory spikes from images
- 120Hz animations dropping frames
This guide shows the real-world performance rules I now follow in all my apps.
These aren’t theoretical — they fix problems you actually hit when building full SwiftUI apps.
Let’s make your UI buttery smooth. 🧈⚡
🧠 1. Break Up Heavy Views Into Smaller Subviews
SwiftUI re-renders the entire View when any @State/@Observed changes.
This is bad:
VStack {
Header()
ExpensiveList(items: items) // ← huge view
}
.onChange(of: searchQuery) {
// whole view recomputes
}
This is better:
VStack {
Header()
ExpensiveList(items: items) // isolated
}
And this is best:
struct BigScreen: View {
var body: some View {
Header()
BodyContent() // isolated subview prevents extra recomputes
}
}
Small reusable components = huge performance wins.
⚡ 2. Use @StateObject & @observable Correctly
Use @StateObject for objects that should NOT reinitialize:
@StateObject var viewModel = HomeViewModel()
Use @observable for lightweight models:
@Observable
class HomeViewModel { ... }
Use @State for simple values (NOT complex structs).
NEVER store heavy objects inside @State.
🧵 3. Keep async work off the main thread
This is WRONG:
func load() async {
let data = try? API.fetch() // slow
self.items = data // UI frozen
}
This is RIGHT:
func load() async {
let data = try? await Task.detached { await API.fetch() }.value
await MainActor.run { self.items = data }
}
Never block the main thread — SwiftUI is 100% dependent on it.
🌀 4. Use .drawingGroup() for heavy drawing
If you render:
- gradients
- blur layers
- large symbols
- complex masks
→ it gets expensive fast.
MyComplexShape()
.drawingGroup()
This forces a GPU render pass → MUCH faster.
🖼 5. Optimize Images (Most Common Lag Source)
Use resized thumbnails:
Image(uiImage: image.resized(to: 200))
Avoid loading large images in ScrollView
Use:
- .resizable().scaledToFit()
- .interpolation(.medium)
- async loading + caching
For remote images:
Use URLCache or Nuke.
📏 6. Avoid Heavy Layout Work Inside ScrollViews
Common mistake:
ScrollView {
ForEach(items) { item in
ExpensiveLayout(item: item)
}
}
Inside scrolling, expensive layout = stutter.
Solution:
- reduce modifiers
- cache computed values
- break child views into isolated components
🧩 7. Prefer LazyVGrid / LazyVStack Over Lists (When Needed)
List is great — until it's not.
Use LazyVStack when:
- custom animations needed
- large compositional layouts
- complex containers inside rows
Use List when:
- your rows are simple
- you want native cell optimizations
🔄 8. Avoid Recomputing Views With .id(UUID())
This forces full view reloads:
MyView()
.id(UUID()) // BAD - destroys identity
Every frame becomes a full redraw.
Only use .id(...) for controlled resets.
.
🧮 9. Computed Properties Should Be Fast
Bad:
var filtered: [Item] {
hugeArray.filter { ... } // expensive
}
Every render = recompute.
Better:
@State private var filtered: [Item] = []
Update when needed:
.onChange(of: searchQuery) { _ in
filtered = hugeArray.filter { ... }
}
🚀 10. Use Transaction to Control Animation Cost
Default animations sometimes stutter.
Smooth it:
withTransaction(Transaction(animation: .snappy)) {
isOpen.toggle()
}
Custom animation transactions = fewer layout jumps.
🏎 11. Turn Off Animations During Bulk Updates
withAnimation(.none) {
items = newItems
}
Prevents lag during large list operations.
🛠 12. Use Instruments (YES, It Works With SwiftUI)
Profile:
- SwiftUI “Dirty Views”
- Memory Graph
- Time Profiler
- Allocation Tracking
- FPS drops
90% of lag comes from:
- huge views
- expensive init
- images
- main-thread blocking
✔️ Final Performance Checklist
Before shipping, ensure:
- [ ] No main-thread API calls
- [ ] No expensive computed properties
- [ ] No layout thrashing inside ScrollViews
- [ ] Images are resized or cached
- [ ] Heavy views split into components
- [ ] ViewModels use @StateObject or @observable
- [ ] Animations use .snappy or .spring and are isolated
- [ ] Lists use Lazy containers when needed
Smooth apps feel premium — and now you know how to build them.
Top comments (0)