swiftui-debugging

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SwiftUI Performance Debugging

SwiftUI性能调试

Systematic guide for diagnosing and fixing SwiftUI performance problems: unnecessary view re-evaluations, identity issues, expensive body computations, and lazy loading mistakes.
一份系统化的指南,用于诊断和修复SwiftUI性能问题:不必要的视图重评估、标识问题、耗时的body计算以及懒加载错误。

When This Skill Activates

何时启用此技能

Use this skill when the user:
  • Reports slow or janky SwiftUI views
  • Sees excessive view re-renders or body re-evaluations
  • Asks about
    Self._printChanges()
    or view debugging
  • Has scrolling performance issues with lists or grids
  • Asks why a view keeps updating when nothing changed
  • Mentions
    @Observable
    or
    ObservableObject
    performance differences
  • Wants to understand SwiftUI view identity or diffing
  • Uses
    AnyView
    and asks about performance implications
  • Has a hang or stutter traced to SwiftUI rendering
当用户出现以下情况时使用此技能:
  • 反馈SwiftUI视图运行缓慢或卡顿
  • 发现视图过度重渲染或body重复评估
  • 询问
    Self._printChanges()
    或视图调试相关内容
  • 列表或网格存在滚动性能问题
  • 疑惑为何视图在无变更时持续更新
  • 提及
    @Observable
    ObservableObject
    的性能差异
  • 想要了解SwiftUI视图标识或差异对比机制
  • 使用
    AnyView
    并询问其性能影响
  • 出现可追溯到SwiftUI渲染的卡顿或停滞

Decision Tree

决策树

What SwiftUI performance problem are you seeing?
|
+- Views re-render when they should not
|  +- Read body-reevaluation.md
|     +- Self._printChanges() to identify which property changed
|     +- @Observable vs ObservableObject observation differences
|     +- Splitting views to narrow observation scope
|
+- Scrolling is slow / choppy (lists, grids)
|  +- Read lazy-loading.md
|     +- VStack vs LazyVStack, ForEach without lazy container
|     +- List prefetching, grid cell reuse
|
+- Views lose state unexpectedly / animate when they should not
|  +- Read view-identity.md
|     +- Structural vs explicit identity
|     +- .id() misuse, conditional view branching
|
+- Known pitfall (AnyView, DateFormatter in body, etc.)
|  +- Read common-pitfalls.md
|     +- AnyView type erasure, object creation in body
|     +- Over-observation, expensive computations
|
+- General "my SwiftUI app is slow" (unknown cause)
|  +- Start with body-reevaluation.md, then common-pitfalls.md
|  +- Use Instruments SwiftUI template (see Debugging Tools below)
你遇到的SwiftUI性能问题是什么?
|
+- 视图在不应重渲染时发生重渲染
|  +- 阅读body-reevaluation.md
|     +- 使用Self._printChanges()确定哪个属性发生了变更
|     +- @Observable与ObservableObject的观察差异
|     +- 拆分视图以缩小观察范围
|
+- 滚动缓慢/卡顿(列表、网格)
|  +- 阅读lazy-loading.md
|     +- VStack与LazyVStack对比,无懒加载容器的ForEach
|     +- List预加载、网格单元格复用
|
+- 视图意外丢失状态/不应动画时出现动画
|  +- 阅读view-identity.md
|     +- 结构性标识与显式标识
|     +- .id()误用、条件视图分支
|
+- 已知陷阱(AnyView、body中的DateFormatter等)
|  +- 阅读common-pitfalls.md
|     +- AnyView类型擦除、body中的对象创建
|     +- 过度观察、耗时计算
|
+- 通用问题“我的SwiftUI应用运行缓慢”(原因未知)
|  +- 从body-reevaluation.md开始,然后查看common-pitfalls.md
|  +- 使用Instruments的SwiftUI模板(见下方调试工具)

API Availability

API可用性

API / TechniqueMinimum VersionReference
Self._printChanges()
iOS 15body-reevaluation.md
@Observable
iOS 17 / macOS 14body-reevaluation.md
@ObservableObject
iOS 13body-reevaluation.md
LazyVStack
/
LazyHStack
iOS 14lazy-loading.md
LazyVGrid
/
LazyHGrid
iOS 14lazy-loading.md
.id()
modifier
iOS 13view-identity.md
Instruments SwiftUI templateXcode 14+SKILL.md
os_signpost
iOS 12SKILL.md
API / 技术最低版本参考
Self._printChanges()
iOS 15body-reevaluation.md
@Observable
iOS 17 / macOS 14body-reevaluation.md
@ObservableObject
iOS 13body-reevaluation.md
LazyVStack
/
LazyHStack
iOS 14lazy-loading.md
LazyVGrid
/
LazyHGrid
iOS 14lazy-loading.md
.id()
修饰符
iOS 13view-identity.md
Instruments SwiftUI模板Xcode 14+SKILL.md
os_signpost
iOS 12SKILL.md

Top 5 Mistakes -- Quick Reference

五大常见错误——快速参考

#MistakeFixDetails
1Large
ForEach
inside
VStack
or
ScrollView
without lazy container
Wrap in
LazyVStack
-- eager
VStack
creates all views upfront
lazy-loading.md
2Using
AnyView
to erase types
Use
@ViewBuilder
,
Group
, or concrete generic types --
AnyView
defeats diffing
common-pitfalls.md
3Creating objects in
body
(
DateFormatter()
,
NumberFormatter()
)
Use
static let
shared instances or
@State
for mutable objects
common-pitfalls.md
4Observing entire model when only one property is neededSplit into smaller
@Observable
objects or extract subviews
body-reevaluation.md
5Unstable
.id()
values causing full view recreation every render
Use stable identifiers (database IDs, UUIDs), never array indices or random valuesview-identity.md
#错误修复方案详情
1
VStack
ScrollView
中使用大型
ForEach
却未使用懒加载容器
包裹在
LazyVStack
中——非懒加载的
VStack
会提前创建所有视图
lazy-loading.md
2使用
AnyView
进行类型擦除
使用
@ViewBuilder
Group
或具体的泛型类型——
AnyView
会破坏差异对比机制
common-pitfalls.md
3
body
中创建对象(
DateFormatter()
NumberFormatter()
使用
static let
共享实例或
@State
存储可变对象
common-pitfalls.md
4观察整个模型但仅需其中一个属性拆分为更小的
@Observable
对象或提取子视图
body-reevaluation.md
5不稳定的
.id()
值导致每次渲染时视图完全重建
使用稳定标识符(数据库ID、UUID),切勿使用数组索引或随机值view-identity.md

Debugging Tools

调试工具

Self._printChanges()

Self._printChanges()

Add to any view body to see what triggered re-evaluation:
swift
var body: some View {
    let _ = Self._printChanges()
    // ... view content
}
Output reads:
ViewName: @self, @identity, _propertyName changed.
See body-reevaluation.md for full interpretation guide.
添加到任意视图的body中,查看触发重评估的原因:
swift
var body: some View {
    let _ = Self._printChanges()
    // ... 视图内容
}
输出示例:
ViewName: @self, @identity, _propertyName changed.
查看body-reevaluation.md获取完整解读指南。

Instruments SwiftUI Template

Instruments SwiftUI模板

  1. Xcode > Product > Profile (Cmd+I)
  2. Choose SwiftUI template (includes View Body, View Properties, Core Animation Commits)
  3. Record, reproduce the slow interaction, stop
  4. View Body lane shows which views had their body evaluated and how often
  5. View Properties lane shows which properties changed
  1. Xcode > Product > Profile(快捷键Cmd+I)
  2. 选择SwiftUI模板(包含View Body、View Properties、Core Animation Commits)
  3. 开始录制,复现缓慢交互后停止
  4. View Body轨道显示哪些视图的body被评估以及评估频率
  5. View Properties轨道显示哪些属性发生了变更

os_signpost for Custom Measurement

os_signpost自定义测量

swift
import os

private let perfLog = OSLog(subsystem: "com.app.perf", category: "SwiftUI")

var body: some View {
    let _ = os_signpost(.event, log: perfLog, name: "MyView.body")
    // ... view content
}
View in Instruments with the os_signpost instrument to count body evaluations per second.
swift
import os

private let perfLog = OSLog(subsystem: "com.app.perf", category: "SwiftUI")

var body: some View {
    let _ = os_signpost(.event, log: perfLog, name: "MyView.body")
    // ... 视图内容
}
在Instruments中使用os_signpost工具查看每秒的body评估次数。

Review Checklist

检查清单

View Identity

视图标识

  • No unstable
    .id()
    values (random, Date(), array index on mutable arrays)
  • Conditional branches (
    if
    /
    else
    ) do not cause unnecessary view destruction
  • ForEach
    uses stable, unique identifiers from the model
  • 无不稳定的
    .id()
    值(随机值、Date()、可变数组的索引)
  • 条件分支(
    if
    /
    else
    )不会导致不必要的视图销毁
  • ForEach
    使用模型中稳定且唯一的标识符

Body Re-evaluation

Body重评估

  • Views observe only the properties they actually use
  • @Observable
    classes preferred over
    ObservableObject
    (iOS 17+)
  • No unnecessary
    @State
    changes that trigger body re-evaluation
  • Large views split into smaller subviews to narrow observation scope
  • 视图仅观察实际使用的属性
  • iOS 17+优先使用
    @Observable
    类而非
    ObservableObject
  • 无触发body重评估的不必要
    @State
    变更
  • 大型视图拆分为更小的子视图以缩小观察范围

Lazy Loading

懒加载

  • Large collections use
    LazyVStack
    /
    LazyHStack
    , not
    VStack
    /
    HStack
  • List
    or lazy stack used for 50+ items
  • No
    .frame(maxHeight: .infinity)
    on children inside lazy containers (defeats laziness)
  • 大型集合使用
    LazyVStack
    /
    LazyHStack
    而非
    VStack
    /
    HStack
  • 50+条目的内容使用
    List
    或懒加载栈
  • 懒加载容器内的子视图未使用
    .frame(maxHeight: .infinity)
    (会破坏懒加载机制)

Common Pitfalls

常见陷阱

  • No
    AnyView
    type erasure (use
    @ViewBuilder
    or
    Group
    )
  • No object allocation in
    body
    (
    DateFormatter
    ,
    NSPredicate
    , view models)
  • Expensive computations moved to background with
    task { }
    or
    Task.detached
  • Images use
    AsyncImage
    or
    .resizable()
    with proper sizing, not raw
    UIImage
    decoding in body
  • 未使用
    AnyView
    进行类型擦除(改用
    @ViewBuilder
    Group
  • body中无对象分配(
    DateFormatter
    NSPredicate
    、视图模型)
  • 耗时计算通过
    task { }
    Task.detached
    移至后台
  • 图片使用
    AsyncImage
    .resizable()
    并设置合适尺寸,未在body中直接解码原始
    UIImage

Reference Files

参考文件

FileContent
view-identity.mdStructural vs explicit identity,
.id()
usage, conditional branching
body-reevaluation.mdWhat triggers body,
_printChanges()
,
@Observable
vs
ObservableObject
lazy-loading.mdLazy vs eager containers,
List
,
ForEach
, grid performance
common-pitfalls.md
AnyView
, object creation in body, over-observation, expensive computations
../profiling/SKILL.mdGeneral Instruments profiling (Time Profiler, Memory, Energy)
文件内容
view-identity.md结构性标识与显式标识、
.id()
用法、条件分支
body-reevaluation.md触发body重评估的原因、
_printChanges()
@Observable
ObservableObject
对比
lazy-loading.md懒加载与非懒加载容器、
List
ForEach
、网格性能
common-pitfalls.md
AnyView
、body中的对象创建、过度观察、耗时计算
../profiling/SKILL.md通用Instruments分析(时间分析器、内存、能耗)