Adam Gelatka5 min

SwiftUI: List vs LazyVStack

iOSEngineeringJan 24, 2025

iOSEngineering

/

Jan 24, 2025

Adam GelatkaiOS Engineer

Share this article

There's always a debate about whether a SwiftUI List is more efficient than a LazyVStack embedded in a ScrollView. By efficiency, I mean in both memory usage and smoothness while scrolling. To explore this (or at least give it a try), I created a lab test comparing the two implementations. The core of the test is a view called MemoryIntensiveView, which uses high-resolution images to push SwiftUI to its limits.

Both implementations (List and LazyVStack) are powered by the same data to ensure a fair comparison.

let entries: [Int] = Array(0...1000)

In the snippet above, the dataset consists of 1,000 integers.

LazyVStack Version

private var lazyVstackVersion: some View { 
  ScrollView { 
    LazyVStack(spacing: 0) { 
      ForEach(entries, id: \.self) { _ in 
        MemoryIntensiveView() 
      } 
    } 
  } 
}

There’s nothing particularly unique about this snippet. We simply use a ScrollView and, within it, declare a single LazyVStack that is populated with multiple instances of MemoryIntensiveView.

List Version

private var listVersion: some View { 
  List { 
    ForEach(entries, id: \.self) { _ in 
      MemoryIntensiveView() 
    } 
  } 
}

The List version is straightforward. We define a simple List and populate it with multiple instances of MemoryIntensiveView.

Science Time!

Methodology

In comparing the two implementations, I focused on monitoring two key aspects:

  • Memory usage
  • Scrolling performance

While memory usage is easily tracked using Xcode, scrolling performance is measured by recording the time it takes to scroll through the list and counting any stutters or hangs (also using Xcode).

The testing was performed on an iPhone 15 Pro with iOS 17.5.1 📱

Memory Usage

To measure memory usage, I utilized Xcode's built-in Memory Report tool.

The testing procedure was straightforward, following these steps:

  1. Measure memory usage before scrolling (right after app launch).
  2. Scroll all the way down through the scroll view.
  3. Measure memory usage after reaching the bottom.
  4. Scroll back up to the top.
  5. Measure memory usage after returning to the start.

The app was rebuilt between each run.

LazyVStack Version Results

List Version Results

Scrolling Performance

To assess scrolling performance, I conducted a straightforward experiment. I timed how long it took to scroll rapidly from the top to the bottom of the view. During this process, I used Instruments to monitor for any SwiftUI hangs and recorded the total time taken to complete the scroll.

The procedure was as follows:

  1. Start the timer and begin monitoring with Instruments.
  2. Scroll down as quickly as possible.
  3. Stop the timer and finish monitoring with Instruments.

Between every run, the app has been rebuild.

Instruments:

LazyVStack Version Results

List Version Results

I've also tried to use ScrollViewReader and invoking scrollTo(...). Unfortunately that was no use for this experiment, because even with animation, it kind of teleports to the bottom of the scrollview.

Comparison

The results clearly show a difference between the List and LazyVStack approaches. The List demonstrates superior effectiveness in both memory usage and scrolling performance.

Memory Usage Side by Side

Scroll Performance Side by Side

Examining the List vs LazyVStack Memory Usage, it’s clear that List has some additional overhead. It starts with 114.4 MB allocated, compared to just 90.2 MB for LazyVStack.

The differences become even more pronounced once scrolling begins. Since List is built on UITableView, it theoretically supports view reuse. The data supports this theory: after scrolling down, List consumes 128.9 MB of memory, less than the 149 MB used by LazyVStack. Interestingly, when scrolling back up, List reverts to a memory usage of 118.2 MB, while LazyVStack remains at 151.8 MB. This suggests that LazyVStack loads views lazily but struggles to free them, whereas List appears to reuse or release views that are no longer visible.

Memory usage is only one aspect of the experiment; user experience is also crucial. The performance difference between the two implementations is substantial. Scrolling to the bottom took 5.53 seconds with the List, whereas LazyVStacktook a staggering 52.3 seconds. The LazyVStack was notably unresponsive and lagged significantly. This is reflected in the hang counts: List experienced 4.6 hangs, while LazyVStack logged 78.

Although the List was not entirely smooth, it provided a far superior user experience compared to LazyVStack.

Resources

struct MemoryIntensiveView: View { 
  var body: some View { 
    VStack { 
      ForEach(0..<20) { _ in 
        HStack { 
          ForEach(0..<20) { _ in 
            HighResolutionImageView() 
              .frame(width: 10, height: 10) 
            } 
          } 
        } 
      } 
    } 
  } 
  
struct HighResolutionImageView: View { 
  var body: some View { 
    Image(.hires) 
      .resizable() 
        .scaledToFill() 
      } 
    }

Conclusion

The experiment highlights clear differences between List and LazyVStack in SwiftUI — LazyVStack struggles with memory management and smooth scrolling in resource-intensive scenarios. On the other hand, List, leveraging UITableView, outperforms in both memory efficiency and user experience, thanks to its view reuse mechanism. For large datasets and performance-critical apps, List is the better choice.

Share this article