Coding Studio

Learn & Grow together.

How to Optimize SwiftUI Performance — Best Practices and Examples

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.

WrapperBest Use CaseTriggers View Update
@StateLocal, lightweight mutable dataOnly current view
@ObservedObjectExternal data model passed inEvery change in object
@StateObjectOwns an observable objectOnly once (better for heavy models)
@EnvironmentObjectShared data globallyWhen 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 @State or @StateObject to cache data between renders.
  • For remote images, use AsyncImage with 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)
            }
        }
    }
}
  • NotesViewModel recreated 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.

Leave a Reply

Your email address will not be published. Required fields are marked *