LazyVStack and LazyHStack
A regular VStack or HStack eagerly creates every child view up front, which is fine for a handful of fixed elements but becomes a performance problem inside a ScrollView containing hundreds or thousands of rows—every view exists in memory simultaneously even though only a fraction is ever visible on screen. LazyVStack and LazyHStack solve this by creating child views only as they are about to become visible (plus a small buffer), and releasing them again once they scroll far enough off-screen, mirroring how List and UIKit's UITableView/UICollectionView reuse and recycle cells. They're the building block you reach for when you need List-like scrolling efficiency but List's fixed row-and-separator chrome doesn't fit your custom layout.
Cricket analogy: A stadium that printed physical scorecards for every possible ball of a 5-day Test in advance would waste enormous resources; instead, like a scoreboard operator who only updates the display for the current over and recycles old panels, LazyVStack only builds rows as they scroll into view.
Where They're Used: Inside ScrollView
Lazy stacks only provide their laziness benefit when embedded in a ScrollView—outside a ScrollView, a lazy stack behaves essentially like its eager counterpart because there's no scrolling viewport to determine what's currently visible. The typical pattern is ScrollView { LazyVStack { ForEach(data) { ... } } }, which gives you List's memory efficiency while letting you fully customize row appearance, spacing, and the absence of default separators/insets that List imposes.
Cricket analogy: A scoreboard's ball-by-ball ticker only saves effort when it's actually scrolling live through an innings — frozen on a single replay screen, there's no benefit; likewise LazyVStack only pays off inside a ScrollView, and it lets you design a fully custom scorecard layout without List's fixed row template.
struct PhotoFeed: View {
let photos: [Photo]
var body: some View {
ScrollView {
LazyVStack(spacing: 12) {
ForEach(photos) { photo in
PhotoCard(photo: photo)
}
}
.padding(.horizontal)
}
}
}LazyHStack and Horizontal Scrolling
LazyHStack applies the same lazy-loading idea to horizontal scrolling, commonly used for carousels, story rails, or horizontally paginated content: ScrollView(.horizontal) { LazyHStack { ForEach(...) { ... } } }. Combined with .scrollTargetLayout() and .scrollTargetBehavior(.paging) (iOS 17+), LazyHStack rows can snap to page boundaries for a native-feeling carousel without any manual gesture handling.
Cricket analogy: A stadium's horizontally scrolling ticker of live match scores from every ground snaps cleanly to each match card, much like LazyHStack combined with .scrollTargetBehavior(.paging) snaps a horizontal carousel to page boundaries automatically.
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 16) {
ForEach(stories) { story in
StoryCircle(story: story)
.frame(width: 72, height: 72)
}
}
.padding(.horizontal)
}Think of Lazy stacks as SwiftUI's version of cell reuse: just as UITableView dequeues and recycles cells as you scroll, LazyVStack/LazyHStack construct child views only near the visible viewport and discard the ones far off-screen, keeping memory and layout work proportional to what's on screen rather than the full dataset.
Because lazy stacks don't know a child's size until it's actually instantiated near the viewport, they can't precompute total scroll content size as precisely as List does internally—giving every row a wildly different, dynamically computed height can cause scroll position jumps. Prefer roughly predictable row sizing, or explicit frame/aspect ratios, for a smoother experience with large datasets.
- LazyVStack and LazyHStack create child views only as they approach the visible scroll viewport, then discard them again when off-screen.
- Laziness only matters inside a ScrollView—outside one, they behave like eager VStack/HStack.
- They're used when you need List-like memory efficiency but want full control over row appearance without List's default chrome.
- LazyHStack combined with .scrollTargetBehavior(.paging) enables native-feeling snapping carousels (iOS 17+).
- Highly variable, unpredictable row/column sizes can cause scroll position instability with large datasets.
- Conceptually, lazy stacks are SwiftUI's analog to UIKit's cell reuse in UITableView/UICollectionView.
Practice what you learned
1. What is the primary performance benefit of LazyVStack over VStack for a long scrolling list?
2. In what container must LazyVStack be placed for its laziness to actually take effect?
3. What iOS 17+ modifier combination gives a LazyHStack native carousel-style page snapping?
4. Why might highly variable, unpredictable row heights cause scroll position instability in a LazyVStack with a large dataset?
5. When would you prefer LazyVStack over List for a scrolling collection?
Was this page helpful?
You May Also Like
List and ForEach
Learn how List renders scrollable, platform-styled collections and how ForEach generates repeated views from a data collection using stable identity.
List Performance in SwiftUI
Learn how SwiftUI's List actually renders and recycles rows, and the concrete techniques for keeping scrolling smooth in large or complex data sets.
Stacks: VStack, HStack, and ZStack
How SwiftUI's three core layout containers arrange child views vertically, horizontally, and by depth, plus alignment and spacing controls.
Modifiers in SwiftUI
How view modifiers work under the hood, why order matters, and how chained modifiers build up new wrapped view types.