Choosing between LazyVStack, List, and VStack in SwiftUI

Published on: May 8, 2025

SwiftUI offers several approaches to building lists of content. You can use a VStack if your list consists of a bunch of elements that should be placed on top of each other. Or you can use a LazyVStack if your list is really long. And in other cases, a List might make more sense.

In this post, I’d like to take a look at each of these components, outline their strengths and weaknesses and hopefully provide you with some insights about how you can decide between these three components that all place content on top of each other.

We’ll start off with a look at VStack. Then we’ll move on to LazyVStack and we’ll wrap things up with List.

Understanding when to use VStack

By far the simplest stack component that we have in SwiftUI is the VStack. It simply places elements on top of each other:

VStack {
  Text("One")
  Text("Two")
  Text("Three")
}

A VStack works really well when you only have a handful of items, and you want to place these items on top of each other. Even though you’ll typically use a VStack for a small number of items, but there’s no reason you couldn’t do something like this:

ScrollView {
  VStack {
    ForEach(models) { model in 
      HStack {
        Text(model.title)
        Image(systemName: model.iconName)
      }
    }
  }
}

When there’s only a few items in models, this will work fine. Whether or not it’s the correct choice… I’d say it’s not.

If your models list grows to maybe 1000 items, you’ll be putting an equal number of views in your VStack. It will require a lot of work from SwiftUI to draw all of these elements.

Eventually this is going to lead to performance issues because every single item in your models is added to the view hierarchy as a view.

Now let's say these views also contain images that must be loaded from the network. SwiftUI is then going to load these images and render them too:

ScrollView {
  VStack {
    ForEach(models) { model in 
      HStack {
        Text(model.title)
        RemoteImage(url: model.imageURL)
      }
    }
  }
}

The RemoteImage in this case would be a custom view that enables loading images from the network.

When everything is placed in a VStack like I did in this sample, your scrolling performance will be horrendous.

A VStack is great for building a vertically stacked view hierarchy. But once your hierarchy starts to look and feel more like a scrollable list… LazyVStack might be the better choice for you.

Understanding when to use a LazyVStack

The LazyVStack components is functionally mostly the same as a regular VStack. The key difference is that a LazyVStack doesn’t add every view to the view hierarchy immediately.

As your user scrolls down a long list of items, the LazyVStack will add more and more views to the hierarchy. This means that you’re not paying a huge cost up front, and in the case of our RemoteImage example from earlier, you’re not loading images that the user might never see.

Swapping a VStack out for a LazyVStack is pretty straightforward:

ScrollView {
  LazyVStack {
    ForEach(models) { model in 
      HStack {
        Text(model.title)
        RemoteImage(url: model.imageURL)
      }
    }
  }
}

Our drawing performance should be much better with the LazyVStack compared to the regular VStack approach.

In a LazyVStack, we’re free to use any type of view that we want, and we have full control over how the list ends up looking. We don’t gain any out of the box functionality which can be great if you require a higher level of customization of your list.

Next, let’s see how List is used to understand how this compares to LazyVStack.

Understanding when to use List

Where a LazyVStack provides us maximum control, a List provides us with useful features right of the box. Depending on where your list is used (for example a sidebar or just as a full screen), List will look and behave slightly differently.

When you use views like NavigationLink inside of a list, you gain some small design tweaks to make it clear that this list item navigates to another view.

This is very useful for most cases, but you might not need any of this functionality.

List also comes with some built-in designs that allow you to easily create something that either looks like the Settings app, or something a bit more like a list of contacts. It’s easy to get started with List if you don’t require lots of customization.

Just like LazyVStack, a List will lazily evaluate its contents which means it’s a good fit for larger sets of data.

A super basic example of using List in the example that we saw earlier would look like this:

List(models) { model in 
  HStack {
    Text(model.title)
    RemoteImage(url: model.imageURL)
  }
}

We don’t have to use a ForEach but we could if we wanted to. This can be useful when you’re using Sections in your list for example:

List {
  Section("General") {
    ForEach(model.general) { item in 
      GeneralItem(item)
    }
  }

  Section("Notifications") {
    ForEach(model.notifications) { item in 
      NotificationItem(item)
    }
  }
}

When you’re using List to build something like a settings page, it’s even allowed to skip using a ForEach altogether and hardcode your child views:

List {
  Section("General") {
    GeneralItem(model.colorScheme)
    GeneralItem(model.showUI)
  }

  Section("Notifications") {
    NotificationItem(model.newsletter)
    NotificationItem(model.socials)
    NotificationItem(model.iaps)
  }
}

The decision between a List and a LazyVStack for me usually comes down to whether or not I need or want List functionality. If I find that I want little to none of List's features odds are that I’m going to reach for LazyVStack in a ScrollView instead.

In Summary

In this post, you learned about VStack, LazyVStack and List. I explained some of the key considerations and performance characteristics for these components, without digging to deeply into solving every use case and possibility. Especially with List there’s a lot you can do. The key point is that List is a component that doesn’t always fit what you need from it. In those cases, it’s useful that we have a LazyVStack.

You learned that both List and LazyVStack are optimized for displaying large amounts of views, and that LazyVStack comes with the biggest amount of flexibility if you’re willing to implement what you need yourself.

You also learned that VStack is really only useful for smaller amounts of views. I love using it for layout purposes but once I start putting together a list of views I prefer a lazier approach. Especially when i’m dealing with an unknown number of items.

Categories

SwiftUI

Subscribe to my newsletter