SwiftUI Performance Optimization — Smooth UIs, Less Recomputing

Published: (December 2, 2025 at 09:36 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

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
  • 120 Hz 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/@ObservedObject changes.

Bad:

VStack {
    Header()
    ExpensiveList(items: items)   // ← huge view
}
.onChange(of: searchQuery) {
    // whole view recomputes
}

Better:

VStack {
    Header()
    ExpensiveList(items: items)   // isolated
}

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 re‑initialize:

@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

Wrong:

func load() async {
    let data = try? API.fetch()   // slow
    self.items = data             // UI frozen
}

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 depends on it.

4. Use .drawingGroup() for Heavy Drawing

If you render gradients, blur layers, large symbols, or 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 a ScrollView. Prefer:

  • .resizable().scaledToFit()
  • .interpolation(.medium)
  • async loading + caching

For remote images, use URLCache or a library like Nuke.

6. Avoid Heavy Layout Work Inside ScrollViews

Common mistake:

ScrollView {
    ForEach(items) { item in
        ExpensiveLayout(item: item)
    }
}

Expensive layout inside scrolling causes stutter.

Solutions:

  • Reduce modifiers
  • Cache computed values
  • Break child views into isolated components

7. Prefer LazyVGrid / LazyVStack Over List (When Needed)

List is great — until it isn’t.

Use LazyVStack when:

  • Custom animations are needed
  • You have large compositional layouts
  • Rows contain complex containers

Use List when:

  • Rows are simple
  • You want native cell optimizations

8. Avoid Recomputing Views With .id(UUID())

MyView()
    .id(UUID())   // BAD – destroys identity

This forces a full view reload each frame. Use .id(...) only for controlled resets.

9. Computed Properties Should Be Fast

Bad:

var filtered: [Item] {
    hugeArray.filter {  }   // expensive
}

Every render recomputes it.

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 can stutter. Smooth them:

withTransaction(Transaction(animation: .snappy)) {
    isOpen.toggle()
}

Custom animation transactions reduce 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 with:

  • 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
Back to Blog

Related posts

Read more »

Bf-Trees: Breaking the Page Barrier

Hello, I'm Maneshwar. I'm working on FreeDevTools – an online, open‑source hub that consolidates dev tools, cheat codes, and TLDRs in one place, making it easy...

Hello Developer: December 2025

In this edition: Meet the 2025 App Store Award winners. Sign up for new design and Liquid Glass activities in the new year. Check out the latest additions to ou...