Disclosure: This article was created or edited with AI assistance. Revised on 13 October 2026 to remove promotional links and unsupported outcome claims. It is educational guidance, not a verified personal case study or a measured result. Examples are illustrative; use only your own documented experience and measurements.
The sections below present options and illustrative examples. Adapt them to the requirements of your project or role; they do not establish a universal rule or promise a result.
1. Use @State Over @StateObject When Possible
@StateObject creates an observable object that triggers view updates. For simple values, @State is cheaper.
// Slow: creates unnecessary observation overhead
class CounterModel: ObservableObject {
@Published var count = 0
}
struct CounterView: View {
@StateObject var model = CounterModel()
// ...
}
// Fast: direct state management
struct CounterView: View {
@State private var count = 0
// ...
}
Choose a state owner and Observation/ObservableObject integration for the model's lifetime; these wrappers are not interchangeable performance switches.
2. Check equality before using EquatableView
An equality check must include every input that affects rendering. Comparing only the number of items is incorrect: content may change while the count stays the same. Prefer the normal update behavior until you have a complete equality definition and evidence that this optimization helps.
3. Lazy Loading in Lists
Consider a lazy stack when profiling shows that eager construction is costly:
// Slow: renders ALL items immediately
ScrollView {
VStack {
ForEach(items) { item in
ItemRow(item: item)
}
}
}
// Fast: renders only visible items
ScrollView {
LazyVStack {
ForEach(items) { item in
ItemRow(item: item)
}
}
}
Compare lazy and regular containers with a representative dataset; no speedup is assumed.
4. Cache Images Properly
Repeated image loading or decoding is one possible cause of scroll hitches. Confirm it with a trace:
struct CachedAsyncImage: View {
let url: URL?
@State private var image: UIImage?
private static let cache = NSCache<NSURL, UIImage>()
var body: some View {
Group {
if let image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
ProgressView()
}
}
.task(id: url) { await loadImage() }
}
@MainActor
private func loadImage() async {
image = nil
guard let url else { return }
if let cached = Self.cache.object(forKey: url as NSURL) {
image = cached
return
}
guard let (data, _) = try? await URLSession.shared.data(from: url),
let downloaded = UIImage(data: data) else { return }
guard !Task.isCancelled else { return }
Self.cache.setObject(downloaded, forKey: url as NSURL)
image = downloaded
}
}
5. Use .task Instead of .onAppear for Async Work
.task automatically cancels when the view disappears. .onAppear doesn't:
// Bad: potential memory leak and race condition
.onAppear {
Task {
await viewModel.loadData()
}
}
// Good: auto-cancellation on disappear
.task {
await viewModel.loadData()
}
6. Minimize View Identity Changes
SwiftUI uses identity to track views. Changing identity forces a full recreation:
// Slow: new identity on every state change
ForEach(items) { item in
ItemView(item: item)
.id(item.hashValue) // DON'T: hashValue can change
}
// Fast: stable identity
ForEach(items) { item in
ItemView(item: item)
// ForEach uses item.id automatically
}
7. Extract Subviews to Limit Redraws
When one piece of state changes, the entire body recomputes. Extract independent parts:
// Baseline: timer state is owned by the parent
struct ProfileView: View {
@State private var name = "John"
@State private var timer = 0
var body: some View {
VStack {
Text(name) // redraws when timer changes!
Text("\(timer)")
ExpensiveChart() // redraws when timer changes!
}
.task {
while !Task.isCancelled {
do { try await Task.sleep(for: .seconds(1)) }
catch { return }
guard !Task.isCancelled else { return }
timer += 1
}
}
}
}
// Alternative: timer state is owned by a child; profile the effect
struct ProfileView: View {
@State private var name = "John"
var body: some View {
VStack {
Text(name)
TimerView() // isolated redraws
ExpensiveChart()
}
}
}
struct TimerView: View {
@State private var timer = 0
var body: some View {
Text("\(timer)")
.task {
while !Task.isCancelled {
do { try await Task.sleep(for: .seconds(1)) }
catch { return }
guard !Task.isCancelled else { return }
timer += 1
}
}
}
}
8. Use drawingGroup() for Complex Graphics
For views with many layers, shadows, or blending:
ZStack {
ForEach(0..<100) { i in
Circle()
.fill(.blue.opacity(0.3))
.frame(width: CGFloat(i) * 3)
.blur(radius: 2)
}
}
.drawingGroup() // renders offscreen then composites
9. Debounce Search Input
Don't fire a network request on every keystroke:
struct SearchView: View {
@State private var query = ""
@State private var results: [Item] = []
var body: some View {
TextField("Search...", text: $query)
.task(id: query) {
try? await Task.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
let found = await search(query)
guard !Task.isCancelled else { return }
results = found
}
}
}
The .task(id:) modifier restarts the task whenever query changes, and the sleep acts as a natural debounce.
10. Profile Before Optimizing
Use Instruments (Xcode > Product > Profile) before guessing:
- SwiftUI Instrument: Shows body evaluations and update counts
- Time Profiler: Identifies slow functions
- Allocations: Finds memory leaks
The biggest performance wins usually come from fixing one or two hot spots, not from micro-optimizing everything.
Quick Reference
| Problem | Solution |
|---|---|
| Slow list scrolling | LazyVStack + image caching |
| Excessive redraws | Extract subviews + Equatable |
| Work after a view disappears | Inspect task lifetime, cancellation and object ownership |
| Choppy animations | Profile layout/rendering before trying drawingGroup |
| Search lag | Debounce with .task(id:) |
Scope of the code examples
These are separate teaching snippets, not a complete tested application or a production-ready package. Match availability to your deployment target, provide omitted model/service definitions, and check errors, cancellation, accessibility and persistence before adapting them.
No timing or memory result is claimed. Check Apple's SwiftUI performance guide and container guidance before selecting an optimization.
Top comments (0)