swiftui-performance

swiftui-performance

热门

使用代码审查、Instruments 和可重复测量来剖析、诊断和修复 SwiftUI 运行时性能。当 SwiftUI 屏幕渲染缓慢、滚动或动画卡顿、视图 body 更新过多、列表标识混乱、布局工作激增或广泛的 Observation 依赖导致 CPU 开销时使用。涵盖基于证据的排查、SwiftUI Instruments 通道、懒加载容器防护、状态生命周期以及前后验证。

927Star
46Fork
更新于 2026/7/15
SKILL.md
readonly只读
name
swiftui-performance
description

使用代码审查、Instruments 和可重复测量来剖析、诊断和修复 SwiftUI 运行时性能。当 SwiftUI 屏幕渲染缓慢、滚动或动画卡顿、视图 body 更新过多、列表标识混乱、布局工作激增或广泛的 Observation 依赖导致 CPU 开销时使用。涵盖基于证据的排查、SwiftUI Instruments 通道、懒加载容器防护、状态生命周期以及前后验证。

SwiftUI 性能

从可复现的症状到可测量的修复,审计 SwiftUI 视图性能。将动画设计路由到 swiftui-animation,生产遥测路由到 metrickit,所有权/泄漏分析路由到 ios-memgraph-analysis,导航行为路由到 swiftui-navigation,状态架构路由到 swiftui-patterns,布局构建路由到 swiftui-layout-components

目录

工作流决策树

  • 提供代码:先审查代码,并将发现标记为假设。
  • 仅有症状:收集最小的相关视图、数据流、复现步骤、设备、操作系统和构建配置。
  • 审查无结论:在建议大规模重构前,收集 trace 或通道截图。

使用此排查列表进行代码和 trace 分析:

  • 广泛的状态依赖或失效风暴
  • 不稳定的列表标识或根条件切换
  • body 中进行格式化、排序、解码或同步 I/O
  • 布局/几何反馈循环和超大图像
  • 隐式动画应用于大型层次结构

1. 代码优先审查

将排查列表中的每个可疑点映射到具体代码。报告可能的原因并附上代码引用,但将其标记为代码支持的假设,直到 trace 确认开销。当证据缺失时,提出最小复现或测量方法。

2. 引导用户进行性能分析

使用 Release 构建 和真实设备(如果可能)上的 SwiftUI Instruments 模板。复现确切的交互,捕获相关的 SwiftUI 通道、Time Profiler 和 Hangs/Hitches。要求提供 trace 或通道及调用树的截图。

3. 分析与诊断

将相同的排查列表应用于 trace 证据。将长时间或频繁的 SwiftUI 更新与 Time Profiler 调用树和复现的交互相关联。区分 trace 支持的发现和代码支持的假设,并命名下一个能解决剩余不确定性的测量。

4. 修复

应用针对性修复:

  • 缩小状态范围(将 @State/@Observable 移到更靠近叶子视图的位置)。
  • 稳定 ForEach 和列表的标识。
  • 将繁重工作移出 body,改为模型层预计算、在其输入变化时更新的显式派生值、记忆化辅助函数或后台处理。
    仅当视图同时拥有值及其更新生命周期时使用 @State;它不是通用计算缓存。
  • 仅当相等性检查比重算子树的成本更低且比较的输入具有稳定的值语义时,才使用 equatable()
  • 在渲染前对图像进行降采样。
  • 减少布局复杂性,或在可能的情况下使用固定尺寸。

常见代码坏味(及修复)

坏味 需寻找的证据 针对性修复
body 中使用 Formatter、排序、过滤或解码 长时间/频繁的 body 更新,且调用树成本匹配 在输入变化时重新计算;在主 actor 之外降采样/解码
UUID() 或不稳定的 id: \.self 行被重建、状态丢失、更新过多 使用稳定的模型标识
if/else 切换 切换时状态重置或更新峰值 在语义允许时局部化条件内容/修饰符
广泛的模型读取 许多不相关的视图一起更新 传递窄值或将读取移到专注的子视图中
布局期间的几何写入 重复的布局/更新循环 阈值化更改,或用稳定布局替换反馈路径

5. 验证

要求用户重新运行相同的捕获,并与基线指标进行比较。如果提供,总结差异(CPU、帧丢失、内存峰值)。

输出

提供:

  • 简短的指标表(如有前后对比)。
  • 主要问题(按影响排序)。
  • 建议的修复及预估工作量。

Instruments 性能分析

在 Instruments 中使用 SwiftUI 模板(Cmd+I 进行性能分析)。当前的 SwiftUI 通道包括 Update Groups、Long View Body Updates、Long Representable Updates / Representable Updates、Other Long Updates / Other Updates 以及 Cause & Effect Graph。将这些与 Time Profiler 和 Hangs/Hitches 关联。

在调试构建中添加 Self._printChanges() 以记录哪个属性触发了视图更新:

var body: some View {
    #if DEBUG
    let _ = Self._printChanges()  // "MyView: @self, _count changed."
    #endif
    Text("Count: \(count)")
}

完整的性能分析工作流程请参见 references/optimizing-swiftui-performance-instruments.md

标识与生命周期

标识控制视图生命周期和状态。在重复内容中使用稳定的模型 ID,并保留 .id(_:) 更改用于有意重置。在性能分析的热行中,优先使用 @ViewBuilder 或泛型组合而非 AnyView。将根条件分支视为可疑对象——而非自动缺陷——当证据显示状态抖动或昂贵的重建时。

Text(title)
    .foregroundStyle(isHighlighted ? .yellow : .primary)

ForEach(items) { item in
    Row(item: item).id(item.stableID)
}

懒加载模式

当性能分析显示急切构造、布局或更新工作显著时,使用懒加载容器;没有通用的项目数量阈值。将网格/列表构建选择路由到 swiftui-layout-components

防护措施:

  • 屏幕外的视图会从懒加载栈中移除。SwiftUI 可能会短暂保留它们,然后删除视图及其视图本地状态。
  • 如果重要的行状态必须在滚动离开后仍然存在,请将其持久化到行视图之外。
  • 由于预取,body 和布局工作可能在 onAppear 之前发生。不要将 onAppear 作为行渲染所需数据的唯一设置点。
  • onAppearonDisappear 视为可见性信号,而非生命周期保证。
  • ForEach 之前过滤数据;避免使用 if 分支使每个元素产生零或一行。
  • 保持每个 ForEach 元素的顶级子视图数量恒定。如有必要,将行内容包装在稳定容器中。在调试列表/表格慢路径时使用 -LogForEachSlowPath YES
  • 避免绝对内容大小或内容偏移假设;懒加载栈会估算屏幕外尺寸。
  • 避免懒加载行中的几何反馈循环。在将几何变化反馈到行状态之前,优先使用稳定尺寸、布局原语或自定义 Layout

状态与 Observation 优化

Observation 跟踪视图评估期间读取的属性。通过传递窄派生值或将读取移到专注的子视图中来减少扇出。

// 将读取拆分到子视图中,使每个子视图只跟踪其渲染的内容。
struct ProfileView: View {
    let model: ProfileModel
    var body: some View {
        VStack {
            NameRow(model: model)      // 只跟踪 name
            EmailRow(model: model)     // 只跟踪 email
            AvatarView(model: model)   // 只跟踪 avatar
            SettingsForm(model: model) // 只跟踪 settings
        }
    }
}

廉价的计算值可以保持派生;昂贵的转换需要显式的所有者、输入集和刷新触发器。不要将视图模型作为性能仪式添加——先测量,然后将通用状态设计路由到 swiftui-patterns

常见错误

  1. 对 Debug 构建进行性能分析。 Debug 构建包含额外的运行时检查并禁用优化,产生误导性的性能数据。在真实设备上对 Release 构建进行性能分析。
  2. 观察整个模型,而只需要一个属性。 将大型 @Observable 模型拆分为专注的模型,或使用计算属性/闭包来缩小观察范围。
  3. 在 ScrollView 项目中使用几何反馈。 GeometryReader 或嘈杂的几何状态可能导致重复布局。优先使用稳定尺寸、自定义布局或带阈值的窄范围 .onGeometryChange(iOS 16+)。
  4. body 内部调用 DateFormatter()NumberFormatter() 这些创建成本高昂。将其设为静态或移到视图外部。
  5. 对非 Equatable 状态进行动画。 如果 SwiftUI 无法确定相等性,它会每帧重绘。使状态符合 Equatable,然后对简单的值绑定变化使用 .animation(_:value:),或对更窄的修饰符作用域隐式动画使用 .animation(_:body:)
  6. 大型扁平 List 没有标识符。 使用 id: 或使项目 Identifiable,以便 SwiftUI 可以高效地进行差异比较,而不是重建整个列表。
  7. 不必要的 @State 包装对象。 将简单值类型包装在类中用于 @State 会破坏值语义。对结构体使用普通的 @State
  8. 用同步 I/O 阻塞 MainActor 文件读取、大型负载的 JSON 解析和图像解码应在主 actor 之外进行。优先使用非隔离的异步辅助函数或专用 actor;仅在有意打破 actor 继承并自行处理取消时,才使用 Task.detached

审查清单

  • [ ] 没有在 body 内部分配 DateFormatter/NumberFormatter
  • [ ] 大型列表使用 Identifiable 项目或显式 id:
  • [ ] @Observable 模型只暴露视图实际读取的属性
  • [ ] 繁重计算不在 MainActor 上(图像处理、解析)
  • [ ] 懒加载行具有稳定的标识、恒定的顶级行形状和预过滤的数据
  • [ ] 滚动行中的几何变化已阈值化,不会反馈到广泛状态
  • [ ] 行渲染不依赖 onAppear 作为唯一设置点
  • [ ] 隐式动画使用 .animation(_:value:) 处理值绑定变化,或使用 .animation(_:body:) 处理更窄的修饰符作用域
  • [ ] 主线程上没有同步网络/文件 I/O
  • [ ] 性能分析在 Release 构建、真实设备上进行
  • [ ] @State 未用作未指定的缓存;每个派生值都有显式的所有者和刷新触发器
  • [ ] 仅当比较比重计算成本更低且输入具有稳定值语义时,才使用 equatable()
  • [ ] 发现区分代码支持的假设和 trace 支持的证据
  • [ ] @Observable 视图模型是 @MainActor 隔离的;跨越并发边界的类型是 Sendable

参考资料