SwiftUI is fast by default, but small mistakes can kill your app's performance. Here are 10 tips I've learned that will keep your UI buttery smooth.
1. Use @Observable Instead of ObservableObject
iOS 17 introduced the @Observable macro which is more efficient:
// Old way - triggers unnecessary updates
class ViewModel: ObservableObject {
@Published var name = ""
@Published var age = 0
}
// New way - only updates when accessed properties change
@Observable
class ViewModel {
var name = ""
var age = 0
}
The new macro tracks only the properties your view actually reads.
2. Avoid Expensive Computations in body
Never do heavy work inside your view's body:
// BAD - computed every render
var body: some View {
let filtered = items.filter { $0.isActive }.sorted()
List(filtered) { item in
Text(item.name)
}
}
// GOOD - cached in ViewModel
var body: some View {
List(viewModel.filteredItems) { item in
Text(item.name)
}
}
3. Use LazyVStack and LazyHStack
Regular stacks load all content immediately. Lazy stacks only load visible items:
// BAD - loads 10,000 items at once
ScrollView {
VStack {
ForEach(items) { item in
ExpensiveRow(item: item)
}
}
}
// GOOD - loads only visible items
ScrollView {
LazyVStack {
ForEach(items) { item in
ExpensiveRow(item: item)
}
}
}
4. Add Stable IDs to ForEach
SwiftUI uses IDs to track changes. Unstable IDs cause unnecessary redraws:
// BAD - index changes when items are reordered
ForEach(items.indices, id: \.self) { index in
ItemRow(item: items[index])
}
// GOOD - stable ID from data model
ForEach(items) { item in
ItemRow(item: item)
}
5. Use EquatableView for Complex Views
Prevent unnecessary updates by implementing Equatable:
struct ProfileCard: View, Equatable {
let user: User
static func == (lhs: ProfileCard, rhs: ProfileCard) -> Bool {
lhs.user.id == rhs.user.id &&
lhs.user.name == rhs.user.name
}
var body: some View {
// Complex view here
}
}
// Usage
EquatableView(content: ProfileCard(user: user))
6. Cache Images Properly
Don't reload images on every render:
// BAD - reloads every time
AsyncImage(url: imageURL) { image in
image.resizable()
} placeholder: {
ProgressView()
}
// GOOD - use a caching library like Kingfisher or SDWebImage
// Or implement your own cache
struct CachedAsyncImage: View {
let url: URL
@State private var image: UIImage?
var body: some View {
Group {
if let image {
Image(uiImage: image)
.resizable()
} else {
ProgressView()
}
}
.task {
image = await ImageCache.shared.load(url)
}
}
}
7. Use drawingGroup() for Complex Graphics
When you have many overlapping views with effects, use drawingGroup():
// Renders as a single flattened image
ZStack {
ForEach(0..<100) { i in
Circle()
.fill(Color.blue.opacity(0.1))
.frame(width: CGFloat(i * 3))
}
}
.drawingGroup() // Huge performance boost!
8. Debounce Search Input
Don't trigger API calls on every keystroke:
@Observable
class SearchViewModel {
var searchText = ""
var results: [Item] = []
private var searchTask: Task<Void, Never>?
func search() {
searchTask?.cancel()
searchTask = Task {
try? await Task.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
results = try await api.search(searchText)
}
}
}
9. Use task(id:) Correctly
Cancel and restart tasks when data changes:
struct UserDetailView: View {
let userID: Int
@State private var user: User?
var body: some View {
VStack {
if let user {
Text(user.name)
}
}
.task(id: userID) { // Restarts when userID changes
user = try? await api.fetchUser(userID)
}
}
}
10. Profile with Instruments
Use Xcode's Instruments to find real bottlenecks:
- Product → Profile (Cmd+I)
- Choose SwiftUI template
- Look for:
- High "body invocations"
- Long "View body" times
- Frequent "State changes"
Quick Reference
| Problem | Solution |
|---|---|
| Slow scrolling | Use LazyVStack/LazyHStack |
| Stuttering animations | Use drawingGroup() |
| Too many redraws | Use @observable, EquatableView |
| Memory spikes | Cache images, use task(id:) |
| Laggy search | Debounce input |
Want optimized, production-ready components?
Check out my SwiftUI UI Components Pack - 38 components that are:
- Optimized for performance
- Tested on real devices
- Ready to copy-paste
Or get the complete SwiftUI Starter Kit Pro with 5 screens and full architecture already set up.
Build fast apps, faster!
Top comments (0)