一句话结论(工程直觉版)
SwiftUI 的 diff 本质是在比较“前后两次 View 描述树”,
diff 成本 ≈ 失效的 View 数量 × 每个 View 的 body 复杂度。
你要做的不是“让 SwiftUI 不 diff”,
而是 缩小参与 diff 的那棵子树。
一、SwiftUI 的 diff 到底在 diff 什么?
1️⃣ diff 的对象不是 UIView
SwiftUI 比较的是:
旧的 View 描述树(值)
vs
新的 View 描述树(值)
不是在 diff:
- UIView
- CALayer
- 实际渲染结果
2️⃣ SwiftUI 用三件事判断“是不是同一个 View”
Type + Position + Identity
① Type
Text("A") vs Text("B") → 同类型
Text vs Image → 不同类型
② Position(结构位置)
VStack {
A
B
}
A 永远是第 0 个子节点。
③ Identity(显式/隐式)
.id(...)ForEach(id:)@StateObject的创建点
identity 决定:
“这次生成的 View,是不是上一次那个?”
3️⃣ diff 的基本流程(简化)
State 改变
→ 找到依赖该 State 的 View
→ 重新计算 body
→ 生成新的 View 树
→ 深度优先 diff(旧 vs 新)
→ 更新最小 Render Tree
二、什么时候 diff 成本会变得很高?
1️⃣ 状态挂在“过高层级”的 View
struct BigScreen: View {
@State var count = 0 // ❌
...
}
- count 改变
- 整个 BigScreen 子树全部参与 diff
👉 失效半径过大
2️⃣ body 中包含昂贵计算
var body: some View {
let sorted = data.sorted() // ❌ 每次 diff 都重算
return List(sorted) { ... }
}
- diff 之前,body 必须先算完
- 算法复杂度直接乘上 diff 次数
3️⃣ identity 不稳定(最隐蔽)
❌ 错误示例
ForEach(items) { item in
Row(item: item)
}
但 item.id 实际上:
- 每次刷新都会变
- 或者你用的是 index
👉 SwiftUI 认为:
“全部是新 View”
结果:
- diff 退化为 全量销毁 + 重建
4️⃣ .id(UUID()) 滥用(自杀式写法)
VStack {
Content()
}
.id(UUID()) // ❌
这等于告诉 SwiftUI:
“每次 State 变,这个 View 都是全新的”
直接禁用 diff。
5️⃣ List / ForEach 中 Row body 很重
即使 diff 正确:
- 每个 Row 的 body 都会被重新计算
- 数量一大就卡
三、什么时候你需要“主动帮 SwiftUI 减少 diff 成本”?
✅ 情况一:高频变化状态
- 搜索输入
- 拖动
- 动画
- 手势
👉 把 State 下沉到 最小 View
✅ 情况二:大列表 / 树状 UI
List/LazyVStack- 多级 ForEach
👉 确保:
- 稳定 id
- Row 内部状态独立
- 重计算移出 body
✅ 情况三:昂贵派生数据
let filtered = users.filter(...)
👉 放到:
- ViewModel
@State- memoized 计算
✅ 情况四:你知道某个子树“绝不会变”
使用 EquatableView
EquatableView {
StaticBanner()
}
或:
struct Banner: View, Equatable {
static func == (...) -> Bool { true }
}
👉 SwiftUI 可以直接跳过 diff。
✅ 情况五:动画或视觉层级复杂
- 渐变
- 模糊
- 遮罩
- 大量 overlay
👉 把动画状态局部化,避免整个 View 树参与动画 diff。
四、工程级“减 diff 成本”清单
你能做的事(从高收益到低收益)
- 状态下沉(最重要)
- 稳定 identity(ForEach / id)
- 避免 body 中重计算
- 拆 View(缩小 diff 子树)
- EquatableView / equatable()
- 避免 .id(UUID())
- Row 级 ViewModel / State
五、一个真实“性能翻倍”的改动
改前
struct ListView: View {
@State var searchText = ""
@State var users: [User]
var body: some View {
List(users.filter { $0.name.contains(searchText) }) {
UserRow(user: $0)
}
}
}
改后
struct UserList: View {
let users: [User]
let searchText: String
var filtered: [User] { ... }
}
👉 diff 参与节点数直接减少一个数量级。
六、最后一句话(工程直觉)
SwiftUI 的 diff 不是“慢”,而是“你告诉它要比较的东西太多了”。
真正的优化不是 hack diff,而是缩小失效子树。
英文版
6-11. [Architecture Design] How Does SwiftUI's Diffing Mechanism Work? When Should You Actively Reduce Diffing Costs?
The Bottom Line (Engineering Intuition)
SwiftUI's diffing is essentially comparing the "before and after View Description Trees."
Diffing Cost ≈ Number of Invalidated Views × Complexity of each View's
body.
Your goal is not to "stop SwiftUI from diffing," but to shrink the subtree that participates in the diff.
I. What exactly is SwiftUI diffing?
1. The targets are NOT UIViews
SwiftUI compares:
- The Old View Description Tree (Value) vs.
- The New View Description Tree (Value)
It is not diffing:
UIViewinstancesCALayerinstances- Actual rendering pixels
2. The "Three Pillars" for identifying a View
SwiftUI uses these three factors to determine if a View is "the same":
Type + Position + Identity
① Type
Swift
Text("A") vs Text("B") // Same Type
Text vs Image // Different Type
② Position (Structural Location)
Swift
VStack {
A
B
}
View A is always the 0th child node.
③ Identity (Explicit/Implicit)
.id(...)ForEach(id:)- The creation point of a
@StateObject
Identity answers the question:
"Is the View generated this time the same entity as the last one?"
3. The Basic Diffing Workflow (Simplified)
State changes
→ Locate Views dependent on that State
→ Recalculate body
→ Generate new View Tree
→ Depth-First Diff (Old vs. New)
→ Update the minimal Render Tree
II. When does the Diffing Cost become high?
1. State attached at "Too High a Level"
Swift
struct BigScreen: View {
@State var count = 0 // ❌ Bad
...
}
- Every time
countchanges, the entireBigScreensubtree participates in the diff. 👉 Invalidation Radius is too large.
2. Expensive calculations inside the body
Swift
var body: some View {
let sorted = data.sorted() // ❌ Recalculated on every diff
return List(sorted) { ... }
}
- The
bodymust finish executing before diffing can even begin. - Algorithmic complexity is effectively multiplied by the number of diffs.
3. Unstable Identity (The most hidden trap)
❌ Bad Example
Swift
ForEach(items) { item in
Row(item: item)
}
If item.id changes on every refresh (or if you are using array indices for a dynamic list): 👉 SwiftUI concludes: "Everything is a brand-new View." Result: The diffing engine degrades into Total Destruction + Total Reconstruction.
4. Abuse of .id(UUID()) (Self-sabotage)
Swift
VStack {
Content()
}
.id(UUID()) // ❌
This tells SwiftUI: "Every time the state changes, this View is completely new." It effectively disables diffing and forces a hard reset.
III. When do you need to "actively help" SwiftUI reduce diffing costs?
✅ Case 1: High-frequency State Changes
- Search input
- Drags/Sliders
- Animations
- Gestures 👉 Push the State down to the smallest possible View.
✅ Case 2: Large Lists / Complex Tree UIs
List/LazyVStack- Nested
ForEach👉 Ensure: Stable IDs, independent Row state, and moving heavy computations out of thebody.
✅ Case 3: Expensive Derived Data
Swift
let filtered = users.filter(...)
👉 Move this to: ViewModel, @State, or Memoized calculations.
✅ Case 4: When you know a subtree "will never change"
Use EquatableView
Swift
EquatableView {
StaticBanner()
}
👉 SwiftUI can skip the diffing for this branch entirely.
IV. Engineering Checklist: Reducing Diffing Costs
What you can do (Ranked from highest to lowest ROI)
- Push State down (Most important)
- Stable Identity (
ForEach/id) - Avoid heavy computation in
body - Decompose Views (Shrink the diff subtree)
EquatableView/.equatable()- Avoid
.id(UUID()) - Row-level ViewModel or State
V. A Real-world "Performance Doubling" Change
Before
Swift
struct ListView: View {
@State var searchText = ""
@State var users: [User]
var body: some View {
List(users.filter { $0.name.contains(searchText) }) {
UserRow(user: $0)
}
}
}
After
Swift
struct UserList: View {
let users: [User]
let searchText: String
var filtered: [User] { ... }
}
👉 The number of nodes participating in the diff is reduced by an order of magnitude.
Final Takeaway (Engineering Intuition)
SwiftUI's diffing isn't "slow"; it’s that "you are giving it too much to compare."
True optimization isn't about hacking the diffing engine—it's about shrinking the invalidation subtree.