How to Optimize SwiftUI Performance — Best Practices and Examples
November 5, 2025
SwiftUI is powerful, declarative, and elegant — but it’s easy to hit performance bottlenecks when your app scales.
If your views re-render too often, animations feel laggy, or scrolling stutters, it’s time to look at SwiftUI performance optimization.
In this post, you’ll learn why SwiftUI apps slow down, and how to fix them using real-world techniques and examples.

1. Understand How SwiftUI Renders Views
Before optimizing, it’s important to understand how SwiftUI updates views.
SwiftUI is declarative — which means:
- When your app’s state changes, SwiftUI rebuilds affected view hierarchies.
- Every render pass compares the new view tree with the old one (a process called diffing) and updates only the changed parts.
So, the key to performance is minimizing unnecessary state changes and view recomputations.
2. Use the Right State Property Wrappers
Choosing the correct property wrapper can make or break your SwiftUI performance.
| Wrapper | Best Use Case | Triggers View Update |
|---|---|---|
@State | Local, lightweight mutable data | Only current view |
@ObservedObject | External data model passed in | Every change in object |
@StateObject | Owns an observable object | Only once (better for heavy models) |
@EnvironmentObject | Shared data globally | When the observed object changes |
Example: Using @StateObject Instead of @ObservedObject
// ❌ Inefficient
struct ContentView: View {
@ObservedObject var viewModel = NotesViewModel() // Recreated every render
var body: some View { NotesList(viewModel: viewModel) }
}
// ✅ Optimized
struct ContentView: View {
@StateObject private var viewModel = NotesViewModel() // Created once
var body: some View { NotesList(viewModel: viewModel) }
}
Result: ViewModel is created once → fewer re-renders → smoother scrolling.
3. Avoid Heavy Computations in the Body
Your body is recomputed often.
Don’t perform data processing or network calls inside it.
Bad Example
var body: some View {
VStack {
Text(expensiveCalculation()) // costly computation here
}
}
Good Example
@State private var result = ""
var body: some View {
VStack {
Text(result)
}
.task {
result = await fetchData() // done asynchronously, outside render
}
}
Tip: Move logic to the ViewModel or use .task {} and onAppear {}.
4. Break Down Complex Views into Smaller Components
Large view hierarchies re-render unnecessarily if not separated properly.
Example
struct MainView: View {
var body: some View {
VStack {
HeaderView()
ContentListView() // separate state
FooterView()
}
}
}
Each subview gets its own body, so SwiftUI can intelligently re-render only changed components.
5. Use EquatableView for Expensive Views
If a view’s content doesn’t change often, wrap it with EquatableView to prevent unnecessary updates.
Example:
struct ProfileView: View, Equatable {
let profile: UserProfile
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.profile.id == rhs.profile.id
}
var body: some View {
VStack {
Text(profile.name)
Text(profile.bio)
}
}
}
Or simpler:
EquatableView(content: ProfileView(profile: user))
SwiftUI skips re-rendering if data is unchanged → huge win for long lists.
6. Optimize Lists and Lazy Stacks
Use LazyVStack or List Instead of VStack for Long Content
ScrollView {
LazyVStack {
ForEach(items) { item in
RowView(item: item)
}
}
}
Lazy containers render only visible cells, like RecyclerView in Android — saving memory and CPU.
Bonus Tip:
Always give list items unique id to help SwiftUI diff efficiently:
ForEach(items, id: \.id) { item in ... }
7. Handle State Updates Efficiently
Avoid Global State Updates
Large environment objects can trigger re-renders across the app.
Instead, use scoped ViewModels and local state.
Example:
// BAD: One global environment object
.environmentObject(AppViewModel())
// GOOD: Scoped ViewModels
@StateObject var profileVM = ProfileViewModel()
@StateObject var settingsVM = SettingsViewModel()
8. Reduce Animation Overhead
Use .transaction and .animation(nil) Smartly
When updating large lists, you can disable implicit animations.
withAnimation(.none) {
items.append(newItem)
}
or
.transaction { $0.animation = nil }
Too many implicit animations = dropped frames.
9. Cache Expensive Operations
If you’re displaying images or computed layouts, cache them.
- Use
@Stateor@StateObjectto cache data between renders. - For remote images, use
AsyncImagewith a caching layer like Nuke or SDWebImageSwiftUI.
10. Debug Performance Using SwiftUI Tools
Use Xcode’s built-in tools:
- Debug → View Rendering → Highlight SwiftUI Updates
→ Shows which parts of your UI re-render. - Instruments → SwiftUI Timeline
→ Profile rendering and layout performance.
Example: Before & After Optimization
Before
struct NotesListView: View {
@ObservedObject var viewModel = NotesViewModel()
var body: some View {
VStack {
ForEach(viewModel.notes) { note in
NoteRow(note: note)
}
}
}
}
NotesViewModelrecreated every render- No lazy loading
- Full re-render on any note change
After
struct NotesListView: View {
@StateObject private var viewModel = NotesViewModel()
var body: some View {
ScrollView {
LazyVStack {
ForEach(viewModel.notes) { note in
NoteRow(note: note)
}
}
}
}
}
- Uses
@StateObject - Lazy loading
- Re-renders only changed rows
Result: Memory ↓ 30%, frame rate ↑ 25%, smoother scrolls.
Final Thoughts
Optimizing SwiftUI isn’t about avoiding updates — it’s about controlling when and how they happen.
Follow these principles:
- Keep your state minimal and scoped.
- Use the right property wrappers.
- Defer expensive work.
- Test performance regularly.
When done right, SwiftUI feels fast, fluid, and delightful — even on older devices.