Lazy-loading views with LazyVStack in SwiftUI

Lazy-loading views with LazyVStack in SwiftUI

Understand lazy loading to load views only when they appear in a SwiftUI app.

When building SwiftUI apps that display lists of content, VStack is usually the first choice to arrange views vertically. This container works perfectly for a short amount of content, but if you're dealing with larger datasets, performance issues may become apparent.

This is where LazyVStack can help. Unlike the VStack container, which creates all its child views immediately, LazyVStack only creates views when they need to be rendered on screen, a process called lazy loading.

Let's explore when and how to implement LazyVStack to improve your app's performance.


LazyVStack is SwiftUI's solution for displaying large amounts of vertically arranged content in a more efficient way. The key difference lies in when views are created:

  • VStack: Creates all child views immediately when the parent view loads;
  • LazyVStack: Creates child views only when they're about to become visible.

Consider this scenario with a regular VStack:

ScrollView {
    VStack {
        ForEach(0..<1000, id: \.self) { index in
            ContentView(index: index)
        }
    }
}

This means that all the 1000 instances of ContentView are created immediately, even if they're not visible on screen. This can lead to:

  • high memory usage, because all the views exist simultaneously;
  • slow initial loading, as the app must load 1000 views before starting to show anything;
  • poor performance for scrolling, as the system fails to manage all these views at once.

The LazyVStack might be the key to solving these issues, as it addresses these issues through lazy evaluation:

ScrollView {
    LazyVStack {
        ForEach(0..<1000, id: \.self) { index in
            ContentView(index: index)
        }
    }
}

Using a LazyVStack reduce memory usage by 80–90% compared to an equivalent VStack, the initial loading decreases from seconds to milliseconds and the scroll performance maintains 60fps even with hundreds of items.

In a real-world scenario, for example, a social media feed with 200 posts using VStack might consume 300MB of RAM and take 2-3 seconds to load. The same content with LazyVStack uses approximately 40MB and loads in under 100ms.

When do you need a LazyVStack?

If your project currently uses VStack, you might notice the following indicators suggesting to switch to LazyVStack.

  • If you're seeing memory warnings or crashes when displaying lists, especially on older devices, your VStack is likely creating too many views at once.
  • When your view takes more than 500ms to appear, particularly with dynamic content from APIs or databases, lazy loading can help.
  • Choppy scrolling, dropped frames, or delayed response to user input often indicate too many views in memory.

LazyVStack uses a technique called viewport-based rendering. Here's how it works:

  1. Initial Load: Creates only the views needed to fill the visible area plus a small buffer;
  2. Scroll Detection: Monitors scroll position to determine when new views should be created;
  3. View Creation: Creates views just before they enter the visible area;
  4. View Destruction: Removes views that have scrolled far out of view to free memory.
Unlike VStack, views in LazyVStack can be created and destroyed multiple times as users scroll back and forth.

How to implement LazyVStack?

The basic syntax for LazyVStack is nearly identical to VStack:

// Before: VStack
VStack(alignment: .leading, spacing: 8) {
    ForEach(items, id: \.id) { item in
        ContentView(item: item)
    }
}

// After: LazyVStack
LazyVStack(alignment: .leading, spacing: 8) {
    ForEach(items, id: \.id) { item in
        ContentView(item: item)
    }
}

Converting a VStack to a LazyVStack is very straightforward, but there are a couple of things that it's important to take notice of:

  • Unlike VStack, LazyVStack requires a ScrollView parent to function properly:
// Before
VStack {
    // content
}

// After
ScrollView {
    LazyVStack {
        // content
    }
}
  • The ForEach inside the LazyVStack should use stable, unique identifiers.

It's important to notice that since views in LazyVStack are created and destroyed as needed, you should avoid storing critical state directly in child views. Instead, use a centralized state management approach with @StateObject@ObservableObject, or SwiftUI's @Observable macro (iOS 17+).

And that's it! By implementing LazyVStack, you've unlocked smooth, memory-efficient scrolling in SwiftUI. Whether you're building feeds, galleries, or data-heavy lists, your users will experience the fluid performance that keeps your app responsive on any device!