SwiftUI 性能模式参考
目录
- 性能优化
- 反模式
- 汇总清单
性能优化
1. 避免冗余的状态更新
SwiftUI 在触发更新前不会比较值:
// 坏 - 即使值未变也会触发更新.onReceive(publisher){valueinself.currentValue=value// 总是触发 body 重新求值}// 好 - 仅在值不同时更新.onReceive(publisher){valueinifself.currentValue!=value{self.currentValue=value}}2. 优化热点路径
热点路径是频繁执行的代码(滚动处理器、动画、手势):
// 坏 - 每次滚动位置变化都更新状态.onPreferenceChange(ScrollOffsetKey.self){offsetinshouldShowTitle=offset.y<=-32// 滚动期间持续触发!}// 好 - 仅在跨越阈值时更新.onPreferenceChange(ScrollOffsetKey.self){offsetinletshouldShow=offset.y<=-32ifshouldShow!=shouldShowTitle{shouldShowTitle=shouldShow// 仅在跨越阈值时触发}}3. 只传递视图需要的东西
避免传递大型的"config"或"context"对象。只传递每个视图需要的具体值。
// 好 - 传递具体值ThemeSelector(theme:config.theme)FontSizeSlider(fontSize:config.fontSize)// 避免 - 传递整个 config(创建宽泛依赖)ThemeSelector(config:config)// 被通知 ALL config 的变化使用ObservableObject时,任何@Published变化都会触发所有观察者。使用@Observable时,视图只会在被访问的属性变化时更新,但传递整个对象仍然会创建比必要更宽的依赖。
4. 使用 Equatable 视图
对于 body 开销大的视图,遵循Equatable:
structExpensiveView:View,Equatable{letdata:SomeDatastaticfunc==(lhs:Self,rhs:Self)->Bool{lhs.data.id==rhs.data.id// 自定义相等性检查}varbody:someView{// 开销大的计算}}// 使用方式ExpensiveView(data:data).equatable()// 使用自定义相等性注意:如果向视图添加新的状态或依赖,记得更新你的==函数!
5. 用于快速 diff 的 POD 视图
POD(Plain Old Data)视图使用memcmp进行最快的 diff。如果视图只包含简单值类型且没有属性包装器,它就是 POD。
// POD 视图 - 最快的 diffstructFastView:View{lettitle:Stringletcount:Intvarbody:someView{Text("\(title):\(count)")}}// 非 POD 视图 - 使用反射或自定义相等性structSlowerView:View{lettitle:String@StateprivatevarisExpanded=false// 属性包装器使其非 PODvarbody:someView{Text(title)}}高级模式:将开销大的非 POD 视图包裹在 POD 父视图中:
// 用于快速 diff 的 POD 包装器structExpensiveView:View{letvalue:Intvarbody:someView{ExpensiveViewInternal(value:value)}}// 带状态的内部视图privatestructExpensiveViewInternal:View{letvalue:Int@Stateprivatevaritem:Item?varbody:someView{// 开销大的渲染}}原因:POD 父视图使用快速的memcmp比较。只有当value变化时,内部视图才会被 diff。
6. 懒加载
大型集合使用惰性容器:
// 坏 - 立即创建所有视图ScrollView{VStack{ForEach(items){iteminExpensiveRow(item:item)}}}// 好 - 按需创建视图ScrollView{LazyVStack{ForEach(items){iteminExpensiveRow(item:item)}}}iOS 26+ 注意:包含惰性堆栈的嵌套滚动视图现在会自动延迟加载其子视图,直到它们即将出现,与顶层惰性堆栈的行为一致。这对垂直滚动视图内的水平照片轮播等模式有利。
来源:“What’s new in SwiftUI”(WWDC25,会话 256)
7. 任务取消
当视图消失时取消异步工作:
structDataView:View{@Stateprivatevardata:[Item]=[]varbody:someView{List(data){iteminText(item.name)}.task{// 视图消失时自动取消data=awaitfetchData()}}}8. 调试视图更新
使用Self._printChanges()或Self._logChanges()调试意外的视图更新。
structDebugView:View{@Stateprivatevarcount=0@Stateprivatevarname=""varbody:someView{#ifDEBUGlet_=Self._logChanges()// Xcode 15.1+:记录到 com.apple.SwiftUI 子系统#endifVStack{Text("Count:\(count)")Text("Name:\(name)")}}}Self._printChanges():将哪些属性变化打印到标准输出。Self._logChanges()(iOS 17+):使用os_log记录到com.apple.SwiftUI子系统,类别为 “Changed Body Properties”,输出结构化日志。
两者在视图值本身变化时打印@self,在视图的持久数据被回收时打印@identity。
原因:这有助于识别哪些状态变化导致视图更新。将重绘触发器隔离到单一职责的子视图中通常是解决办法——提取子视图意味着当其输入未变化时,SwiftUI 可以跳过其 body。
9. 消除不必要的依赖
收窄状态作用域以减少更新扇出。不要将整个@Observable模型传递给行视图(这会创建对所有被访问属性的依赖),只将视图需要的具体值作为let属性传递。
// 坏 - 对整个模型宽泛依赖structItemRow:View{@Environment(AppModel.self)privatevarmodelletitem:Itemvarbody:someView{Text(item.name).foregroundStyle(model.theme.primaryColor)}}// 好 - 收窄依赖structItemRow:View{letitem:ItemletthemeColor:Colorvarbody:someView{Text(item.name).foregroundStyle(themeColor)}}避免在环境中存储频繁变化的值。对任何环境键的每次写入都会强制检查该子树中读取任何键的每个视图。因此,高频值(滚动偏移、窗口/容器尺寸、拖动手势平移、逐帧动画进度、计时器滴答、悬停位置)流入@Entry或.environment(\.key, value)会形成失效风暴。
相反,将该值保存在@Observable模型中,并暴露一个粗化的值——布尔阈值而不是原始测量值——这样视图只会在跨越有意义的边界时失效:
@MainActor@ObservablefinalclassViewportModel{varwidth:CGFloat=0{didSet{isWide=width>600}}private(set)varisWide=false// 读者只在此值翻转时失效}同样的形状也适用于列表中的每一行:在模型上存储原始偏移并让每行读取它,仍然会让所有可见行每帧失效。给每个项目一个自己的@Observable,其属性只跟踪该项目自身的派生状态(例如isVisible),这样每行最多失效两次(进入/离开),无论滚动速度如何——见下面的第 10 项。
对于完全由滚动位置驱动的视觉效果(不透明度、缩放、旋转),优先使用scrollTransition或visualEffect(in:)——它们将逐帧工作推给渲染器并完全跳过 body 重新求值。只有当滚动派生的值必须驱动非渲染逻辑(模型更新、预取、兄弟状态)时,才使用@Observable+ 粗化模式。
来源:“Optimize SwiftUI performance with Instruments”(WWDC25,会话 306)
10. @Observable 依赖粒度
考虑使用逐项(每行/每项一个)@Observable状态持有者来收窄更新作用域。当多个列表项共享对同一个@Observable数组的依赖时,更改一个元素会导致所有项重新求值其 body。
// 坏 - 所有项视图都依赖整个 favorites 数组@ObservableclassModelData{varfavorites:[Landmark]=[]funcisFavorite(_landmark:Landmark)->Bool{favorites.contains(landmark)}}structLandmarkRow:View{letlandmark:Landmark@Environment(ModelData.self)privatevarmodelvarbody:someView{HStack{Text(landmark.name)ifmodel.isFavorite(landmark){Image(systemName:"heart.fill")}}}}// 好 - 每个项都有自己的可观察视图模型@ObservableclassLandmarkViewModel{varisFavorite:Bool=false}structLandmarkRow:View{letlandmark:LandmarkletviewModel:LandmarkViewModelvarbody:someView{HStack{Text(landmark.name)ifviewModel.isFavorite{Image(systemName:"heart.fill")}}}}原因:使用坏模式时,切换一个收藏会将整个数组标记为已更改,导致每个LandmarkRow重新运行其 body。使用逐项视图模型时,只有被切换项(的 body)运行。
来源:“Optimize SwiftUI performance with Instruments”(WWDC25,会话 306)
11. 非主线程闭包
出于性能考虑,SwiftUI 可能在后台线程上调用某些闭包。这些闭包必须是Sendable的,并且应避免直接访问@MainActor隔离的状态。相反,在闭包的捕获列表中捕获所需的值。
可能在线程外运行的闭包:
Shape.path(in:)visualEffect闭包Layout协议方法onGeometryChange转换闭包
// 坏 - 直接访问 @MainActor 状态.visualEffect{content,geometryincontent.blur(radius:self.pulse?5:0)// 编译错误:@MainActor 隔离}// 好 - 捕获值.visualEffect{[pulse]content,geometryincontent.blur(radius:pulse?5:0)}来源:“Explore concurrency in SwiftUI”(WWDC25,会话 266)
12. 常见性能问题
了解 SwiftUI 中常见的性能瓶颈:
- 宽泛状态变化导致的视图失效风暴
- 列表中不稳定的身份导致过度 diff
body中的重活(格式化、排序、图像解码)- 深层堆栈或偏好链导致的布局抖动
当出现性能问题时,建议用户使用 Instruments(SwiftUI 模板)进行性能剖析,以识别具体瓶颈。
反模式
1. 在 Body 中创建对象
// 坏 - 每次 body 调用都创建新的 formattervarbody:someView{letformatter=DateFormatter()formatter.dateStyle=.longreturnText(formatter.string(from:date))}// 好 - 静态或存储的 formatterprivatestaticletdateFormatter:DateFormatter={letf=DateFormatter()f.dateStyle=.longreturnf}()varbody:someView{Text(Self.dateFormatter.string(from:date))}2. 在 Body 中进行重计算
保持视图 body 简单和纯净。避免副作用、派发或复杂逻辑。
// 坏 - 每次 body 调用都排序数组varbody:someView{List(items.sorted{$0.name<$1.name}){iteminText(item.name)}}// 好 - 计算一次,通过 onChange 或在模型中用计算属性更新@StateprivatevarsortedItems:[Item]=[]varbody:someView{List(sortedItems){iteminText(item.name)}.onChange(of:items){_,newItemsinsortedItems=newItems.sorted{$0.name<$1.name}}}将排序、过滤和格式化移到模型或计算属性中。body应该是状态的纯结构表示。
3. 不必要的状态
// 坏 - 派生状态被单独存储@Stateprivatevaritems:[Item]=[]@StateprivatevaritemCount:Int=0// 不必要!// 好 - 计算派生值@Stateprivatevaritems:[Item]=[]varitemCount:Int{items.count}// 计算属性汇总清单
- 状态更新在赋值前检查值是否变化
- 热点路径最小化状态更新
- 只传递视图需要的值(避免大型 config 对象)
- 大型列表使用
LazyVStack/LazyHStack body中不创建对象- 重计算移出
body - body 保持简单和纯净(无副作用)
- 派生状态是计算的,而不是存储的
- 使用
Self._logChanges()或Self._printChanges()调试意外更新 - 开销大的视图遵循 Equatable(在合适时)
- 考虑 POD 视图包装器进行高级优化
- 考虑对列表项使用细粒度的 @Observable 依赖(当能切实减少更新时,每行使用更小的可观察单元)
- 频繁变化的值不存储在环境中
- Sendable 闭包(Shape、visualEffect、Layout)捕获值而不是访问 @MainActor 状态