DEV Community

Sebastien Lato
Sebastien Lato

Posted on

SwiftUI Performance Optimization — Smooth UIs, Less Recomputing

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
}
Enter fullscreen mode Exit fullscreen mode

This is better:

VStack {
    Header()
    ExpensiveList(items: items)   // isolated
}
Enter fullscreen mode Exit fullscreen mode

And this is best:

struct BigScreen: View {
    var body: some View {
        Header()
        BodyContent()   // isolated subview prevents extra recomputes
    }
}
Enter fullscreen mode Exit fullscreen mode

Small reusable components = huge performance wins.


⚡ 2. Use @StateObject & @observable Correctly

Use @StateObject for objects that should NOT reinitialize:

@StateObject var viewModel = HomeViewModel()
Enter fullscreen mode Exit fullscreen mode

Use @observable for lightweight models:

@Observable
class HomeViewModel { ... }
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

This is RIGHT:

func load() async {
    let data = try? await Task.detached { await API.fetch() }.value
    await MainActor.run { self.items = data }
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

This forces a GPU render pass → MUCH faster.


🖼 5. Optimize Images (Most Common Lag Source)

Use resized thumbnails:

Image(uiImage: image.resized(to: 200))
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Every render = recompute.

Better:

@State private var filtered: [Item] = []
Enter fullscreen mode Exit fullscreen mode

Update when needed:

.onChange(of: searchQuery) { _ in
    filtered = hugeArray.filter { ... }
}
Enter fullscreen mode Exit fullscreen mode

🚀 10. Use Transaction to Control Animation Cost

Default animations sometimes stutter.

Smooth it:

withTransaction(Transaction(animation: .snappy)) {
    isOpen.toggle()
}
Enter fullscreen mode Exit fullscreen mode

Custom animation transactions = fewer layout jumps.


🏎 11. Turn Off Animations During Bulk Updates

withAnimation(.none) {
    items = newItems
}
Enter fullscreen mode Exit fullscreen mode

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)