DEV Community

Sebastien Lato
Sebastien Lato

Posted on

SwiftUI Performance Deep Dive: Rendering, Identity & Invalidations

SwiftUI performance problems rarely come from “slow code”.

They come from misunderstanding how SwiftUI renders views.

That’s why you see:

  • views re-rendering “for no reason”
  • animations resetting unexpectedly
  • lists stuttering
  • state seemingly ignored
  • random layout glitches
  • performance getting worse as the app grows

This post explains how SwiftUI actually renders, what causes invalidations, how identity works, and how to build fast, predictable SwiftUI apps.


🧠 The Core Truth About SwiftUI Performance

SwiftUI is value-based and declarative.

Every time SwiftUI decides something might have changed, it:

  1. Recomputes body
  2. Diffs the new view tree
  3. Decides what to update on screen

Recomputing body is cheap.

Invalidating identity is not.


🔄 1. What Actually Triggers a View Update?

A view updates when:

  • a @State value changes
  • a @StateObject / @Observable property changes
  • a @Binding changes
  • an environment value changes
  • a parent view updates

A view does not update when:

  • unrelated state changes
  • async tasks finish without state mutation
  • services update internally without touching state

Understanding what triggers updates eliminates most performance myths.


🆔 2. View Identity: The #1 Performance Killer

SwiftUI tracks views by identity, not by type.

Bad identity example:

ForEach(items) { item in
    RowView(item: item)
}
Enter fullscreen mode Exit fullscreen mode

If item.id changes or is unstable → SwiftUI thinks the view is new.

Good identity:

ForEach(items, id: \.id) { item in
    RowView(item: item)
}
Enter fullscreen mode Exit fullscreen mode

Rule:
📌 Stable identity = stable performance.


⚠️ 3. The id() Modifier: Powerful and Dangerous

This forces SwiftUI to treat a view as brand new:

Text(title)
    .id(UUID()) // ❌ forces recreation every update
Enter fullscreen mode Exit fullscreen mode

Use id() only when you explicitly want:

  • animation reset
  • scroll reset
  • state reset

Never use it casually.


🔁 4. Body Recomputations Are NOT the Problem

This scares people:

var body: some View {
    print("render")
    ...
}
Enter fullscreen mode Exit fullscreen mode

Yes, it prints often.

That’s fine.

SwiftUI is designed to recompute bodies frequently.

Performance problems come from:

  • recreating ViewModels
  • breaking identity
  • triggering expensive layout
  • invalidating large subtrees

🧠 5. ViewModel Identity & @StateObject

This is a classic bug:

MyView(viewModel: ViewModel()) // ❌ recreated every render
Enter fullscreen mode Exit fullscreen mode

Correct:

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

Or inject once from parent.

If your ViewModel is recreated:

  • async tasks restart
  • caches are lost
  • animations reset
  • performance tanks

📦 6. Lists & Performance Traps

❌ Common mistakes:

  • heavy views inside List
  • GeometryReader in rows
  • unstable IDs
  • large images without caching
  • async work inside row bodies

✔ Best practices:

  • keep rows lightweight
  • precompute data in ViewModel
  • paginate aggressively
  • avoid layout measurement per row
  • cache images

SwiftUI lists are fast if identity and layout are stable.


⚖️ 7. Equatable Views (When to Use Them)

You can reduce updates:

struct RowView: View, Equatable {
    let model: Model

    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.model.id == rhs.model.id &&
        lhs.model.title == rhs.model.title
    }
}
Enter fullscreen mode Exit fullscreen mode

SwiftUI skips rendering when values are equal.

Use for:

  • complex rows
  • dashboards
  • frequently updating parents

Don’t overuse — identity comes first.


📐 8. Layout Is Part of Performance

Layout happens every render pass.

Performance killers:

  • deep nested stacks
  • GeometryReader everywhere
  • dynamic size measurement in lists
  • constantly changing layout constraints

Better tools:

  • intrinsic sizing
  • Layout protocol
  • ViewThatFits
  • caching measured values

Layout inefficiency = render inefficiency.


🔄 9. Environment Updates Are Global Invalidations

Environment values invalidate entire subtrees.

Be careful with:

  • large environment objects
  • frequently changing global state
  • putting fast-changing values in environment

Rule:
📌 Environment = configuration, not volatile state.


🧵 10. Async & Rendering Coordination

Bad pattern:

Task {
    data = await load()
}
Enter fullscreen mode Exit fullscreen mode

Good pattern:

@MainActor
func load() async {
    loading = true
    defer { loading = false }
    data = await service.load()
}
Enter fullscreen mode Exit fullscreen mode

Why?

  • controlled invalidations
  • predictable updates
  • no background thread mutations

🧪 11. Diagnosing Performance Issues

Ask these questions:

  1. Is identity stable?
  2. Is something being recreated?
  3. Is layout expensive?
  4. Is environment invalidating too much?
  5. Is async mutating state repeatedly?
  6. Are lists doing heavy work?

Most bugs reveal themselves immediately.


🧠 Mental Performance Model

Think in layers:

State change
   
View invalidation
   
Body recompute
   
Layout
   
Diff
   
Render
Enter fullscreen mode Exit fullscreen mode

Control invalidations → control performance.


🚀 Final Thoughts

SwiftUI performance isn’t about micro-optimizations.

It’s about:

  • identity
  • ownership
  • predictable data flow
  • controlled invalidation
  • clean architecture

Once you master these, SwiftUI apps scale beautifully — even with complex UI.

Top comments (0)