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:
- Measure memory usage before scrolling (right after app launch).
- Scroll all the way down through the scroll view.
- Measure memory usage after reaching the bottom.
- Scroll back up to the top.
- 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:
- Start the timer and begin monitoring with Instruments.
- Scroll down as quickly as possible.
- 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 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
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.