# iOS Engineer

---

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.

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

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

## LazyVStack Version

```swift
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

```swift
private var listVersion: some View {
    List {
        // Populate with MemoryIntensiveView instances
        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

*Results to be filled in after testing.*

#### List Version Results

*Results to be filled in after testing.*

## 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 rebuilt.

**Instruments:**

*Results to be filled in after testing.*

### LazyVStack Version Results

*Results to be filled in.*

*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

*Results to be filled in.*

### 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 `LazyVStack` took 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

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

struct HighResolutionImageView: 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.

---

Don't miss anything