SwiftUI:List与LazyVStack嵌套性能差异,为何反向嵌套更优?
问题重现
测试SwiftUI中List、ScrollView和LazyVStack的性能差异时,遇到两种嵌套写法的反常表现:
写法1:LazyVStack套ForEach
/// Case 1: `LazyVStack` 包裹 `ForEach` var body: some View { List { LazyVStack { ForEach(data) { data in AsyncImage(url: URL(string: "https://picsum.photos/200/300")) .frame(width: 40, height: 40) } } } }
写法2:ForEach套LazyVStack
/// Case 2: `ForEach` 包裹 `LazyVStack` var body: some View { List { ForEach(data) { data in LazyVStack { AsyncImage(url: URL(string: "https://picsum.photos/200/300")) .frame(width: 40, height: 40) } } } }
原本觉得写法1更合理,结果写法2性能反而更好:写法1滚动时图片内存不会释放,内存一路上涨;写法2滚动时能正常释放图片内存。明明写法2每行都要新建LazyVStack,为啥反而更高效?
核心原因解析
List的单元格复用逻辑被干扰
List本身自带单元格复用优化——只会保持屏幕内(及少量预加载)的单元格存活,移出屏幕的会被销毁回收。但写法1里,List把整个LazyVStack当成单个单元格,不会去处理LazyVStack内部的ForEach元素。这就导致LazyVStack里的所有AsyncImage都会一直留在内存里,滚动时移出屏幕的图片也不会被释放,内存自然持续累积。写法2适配了List的原生优化
写法2里,ForEach直接作为List的子元素,List会把每个带LazyVStack的视图当成独立的单元格。这时List的复用机制正常工作:单元格一移出屏幕,就会被销毁,里面的LazyVStack和AsyncImage也跟着释放内存。虽然每个单元格都有个LazyVStack,但单个LazyVStack只装了一张图,这点开销完全可以忽略,远比不上写法1里内存无法回收的消耗。LazyVStack的适用场景
LazyVStack是给ScrollView这种没有自带复用机制的容器设计的,用来实现懒加载。而List本身已经把复用优化做到位了,在List里套LazyVStack纯属冗余,还会打乱List的原生逻辑,反而降低性能。
结论
写法2性能更优的核心是它顺应了List的原生优化逻辑,让List能正常回收单元格资源;写法1因为嵌套方式错误,干扰了List的复用机制,导致内存无法释放。用List的时候,直接用ForEach生成单元格即可,完全没必要额外套LazyVStack。
内容的提问来源于stack exchange,提问作者gobtronic

