What are the downsides to using lazy stacks?

In Stacks, Grids, and Outlines in SwiftUI, a recommendation is made to only use lazy stacks when you encounter performance bottlenecks.

To quote the session, starting at 4:43:

On the other hand, making the stacks within a given hero view lazy doesn't actually confer any benefits. The content is all visible at once, as soon as the view lands on screen. So everything has to be loaded at once, regardless of the container's default behavior.

If you aren't sure which stack to use, use VStack or HStack. Adopt lazy stacks as a way to resolve performance bottlenecks that you find after profiling with Instruments.

Are there negatives, performance or otherwise, to just always use lazy stacks? I'd like to understand why I shouldn't reach for LazyVStack and LazyHStack first. I understand from the example that using a lazy stack wouldn't confer any benefits, but would we incur any negatives?


Accepted Reply

Lazy stacks incur a small amount of extra overhead, both in time and memory, to handle the bookkeeping for what views have and have not been instantiated. In cases where all the views have to be instantiated anyway, that overhead is pure cost with no benefit.

It’s also worth noting that all the views in a lazy stack must be instantiated in some situations that might be unexpected at first glance. For example consider a vertically scrolling ScrollView containing a LazyVStack. So far, so good. But now suppose that LazyVStack includes horizontally scrolling ScrollViews within it—think rows with photo thumbnails. In the general case, all the views in the horizontally scrolling ScrollView have to be instantiated eagerly so that SwiftUI can determine the height of the row.

These unexpected cases are the reason to prefer regular stacks and switch to lazy stacks when it makes a measurable difference. If lazy stacks were always the right answer, SwiftUI could have just made all stacks lazy.

Practically, if the highest level ScrollView in a view hierarchy directly wraps an HStack or VStack, then that’s a good candidate for a lazy stack. Beyond that, profile first lest you accidentally make your app slower while trying to make it faster.

Replies

Lazy stacks incur a small amount of extra overhead, both in time and memory, to handle the bookkeeping for what views have and have not been instantiated. In cases where all the views have to be instantiated anyway, that overhead is pure cost with no benefit.

It’s also worth noting that all the views in a lazy stack must be instantiated in some situations that might be unexpected at first glance. For example consider a vertically scrolling ScrollView containing a LazyVStack. So far, so good. But now suppose that LazyVStack includes horizontally scrolling ScrollViews within it—think rows with photo thumbnails. In the general case, all the views in the horizontally scrolling ScrollView have to be instantiated eagerly so that SwiftUI can determine the height of the row.

These unexpected cases are the reason to prefer regular stacks and switch to lazy stacks when it makes a measurable difference. If lazy stacks were always the right answer, SwiftUI could have just made all stacks lazy.

Practically, if the highest level ScrollView in a view hierarchy directly wraps an HStack or VStack, then that’s a good candidate for a lazy stack. Beyond that, profile first lest you accidentally make your app slower while trying to make it faster.
I really appreciate your efforts and knowledge in responding to these forums, and this question in particular.

That said, the other metric (besides performance) is ease of coding.

One of the advantages of SwiftUI is its ease of coding. WWDC 2020 "Introduction to SwiftUI" spent some time comparing SwiftUI to UIKit, and came to the conclusion that for UIKit "the way we manually manage dependencies today is really hard". There were several other comments about how much UIKit required the coder to "have to hold in your head", and how SwiftUI reduces this cognitive load.

Great. I've been nattering on and on for years about how complexity causes errors. But now complexity has been (re)introduced to SwiftUI in order to increase performance. Now we are required to know when to use Lazy Stacks and when not to.

Again, thanks for your answer, but I was unable to use it to figure out how to improve my code. It doesn't answer the question about when to use Lazy Stacks and when not to. You gave a few reasons why we should differentiate between Lazy and NonLazy stacks. You gave a brief rule of thumb about when to use Lazy ("if the highest level ScrollView in a view hierarchy directly wraps an HStack or VStack, then that’s a good candidate for a lazy stack").

In addition, even if I understood when to be Lazy and when not to, I still have to retain that knowledge. Why put that cognitive burden on the overworked, confused coder?

Why not make all stacks Lazy stacks, and implement the above rule of thumb in the compiler? If the above rule of thumb is insufficient, what are the exact rules I need to maintain in my (limited) headspace for Lazy Stacks?

It doesn't answer the question about when to use Lazy Stacks and when not to. 

I think it did. First consider the "rule of thumb" while coding; then profile the app and if there are performance issues involving a particular Stack, try changing its laziness and see if that helps.

Why not make all stacks Lazy stacks, and implement the above rule of thumb in the compiler? 

Presumably because the decision about whether a Stack should be lazy is not one that can be made by an algorithm. If that rule of thumb were hardcoded, there would probably be cases where it turned out to be wrong, and the app developer wouldn't be able to do anything about it.

Programming is full of situations where there are multiple ways to do something, with different performance characteristics. Should I implement this property as a precomputed value, or lazily evaluate it when it's accessed? Should I use a search tree or a hash table here? Etc.