Slow scroll views are one of the most noticeable performance problems in SwiftUI apps. Optimizing them can make a significant difference, especially when dealing with large datasets or complex layouts.

Understanding ScrollView Performance

ScrollView in SwiftUI doesn't have built-in view recycling like UITableView or UICollectionView. This means all views are created upfront, which can lead to performance issues with large datasets. However, LazyVStack and LazyHStack provide lazy loading capabilities.

Use LazyVStack and LazyHStack

For better performance, use lazy stacks instead of regular ones inside ScrollView:

import SwiftUI

struct PerformantScrollView: View {
    let items = Array(1...1000)

    var body: some View {
        ScrollView {
            // Good: LazyVStack creates views on demand
            LazyVStack {
                ForEach(items, id: \.self) { item in
                    ItemRow(number: item)
                }
            }
        }
    }
}

struct ItemRow: View {
    let number: Int

    var body: some View {
        Text("Item \(number)")
            .frame(height: 50)
            .frame(maxWidth: .infinity)
            .background(Color.blue.opacity(0.1))
            .cornerRadius(8)
            .padding(.horizontal)
    }
}

Avoid Heavy Computations in Views

Move expensive operations outside the view body:

struct OptimizedItemView: View {
    let item: Item
    let processedData: String // Computed outside the view

    var body: some View {
        VStack {
            Text(item.title)
            Text(processedData)
        }
    }
}

// Instead of:
// var body: some View {
//     Text(expensiveComputation()) // Called on every render — avoid this
// }

Use LazyVGrid and LazyHGrid for Grids

For grid layouts, use lazy grids:

struct GridView: View {
    let items = Array(1...100)
    let columns = [
        GridItem(.adaptive(minimum: 100))
    ]

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns) {
                ForEach(items, id: \.self) { item in
                    RoundedRectangle(cornerRadius: 10)
                        .fill(Color.blue)
                        .frame(height: 100)
                        .overlay(Text("\(item)"))
                }
            }
            .padding()
        }
    }
}

Optimize Image Loading

Use AsyncImage or cached image loading for remote images:

struct ImageListView: View {
    let imageUrls: [URL]

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(imageUrls, id: \.self) { url in
                    AsyncImage(url: url) { image in
                        image.resizable()
                    } placeholder: {
                        Color.gray
                    }
                    .frame(height: 200)
                }
            }
        }
    }
}

Monitor Performance

Use Instruments to profile your scroll views:

  • Time Profiler to identify slow code

  • SwiftUI Profiler to see view updates

  • Memory profiler to check for leaks

Caveats

  • LazyVStack/LazyHStack don't support all modifiers the same way as regular stacks

  • Be careful with onAppear/onDisappear in lazy stacks as they're called when views enter/exit viewport

  • Complex view hierarchies still impact performance even with lazy loading

Wrap up

Optimizing ScrollView performance in SwiftUI involves using lazy stacks, minimizing expensive computations, and being mindful of view creation. These techniques will help you build smooth, responsive scrolling experiences.

Resources:

Read more

Share


Share Bluesky Mastodon Twitter LinkedIn Facebook