SwiftUI 并不慢——是你的代码慢
Source: Dev.to
SwiftUI 渲染:哪些快,哪些慢
SwiftUI 的渲染引擎本身很快。让它变慢的是你让它 每秒重新执行数百次 的工作。
SwiftUI 的工作原理
SwiftUI 没有一次性触发的 viewDidLoad。它使用 声明式差分系统:
- 状态变化 – 你更新
@State、@Binding、@ObservedObject或其他任何真相源。 - 调用
body– SwiftUI 为每个依赖该状态的视图重新评估body。 - 进行差分 – SwiftUI 将新的视图树与之前的视图树进行比较。
- 仅渲染差异 – 如果没有变化,就不会重新绘制。
当 body 的计算代价低 时,这种方式既优雅又高效。
问题在哪儿? SwiftUI 无法判断你的 body 计算是否轻量。如果你在 body 中放入耗时的工作,它将在 每一次状态变化 时重新执行。
Source: …
示例:个人理财仪表盘
struct DashboardView: View {
@State private var selectedPeriod: Period = .thisMonth
@State private var showDetails = false
let transactions: [Transaction]
var body: some View {
// ALL of this recalculates on EVERY body call
// — even when the user just toggles `showDetails`!
let filtered = transactions.filter {
$0.date >= selectedPeriod.startDate &&
$0.date $1.value }
ScrollView {
VStack(spacing: 16) {
PeriodPicker(selection: $selectedPeriod)
SummaryCard(title: "Total", value: totalSpent)
SummaryCard(title: "Daily Avg", value: dailyAverage)
ForEach(byCategory, id: \.key) { cat, amt in
CategoryRow(name: cat, amount: amt)
}
Button("Details") { withAnimation { showDetails.toggle() } }
if showDetails { MerchantList(transactions: filtered) }
}
}
}
}
用户点击 “Details” → showDetails 变化 → body 重新评估 → 每个聚合都重新运行,即使交易数据并未改变。
解决方案:在周期变化时预先计算
struct DashboardView: View {
@State private var selectedPeriod: Period = .thisMonth
@State private var showDetails = false
@State private var summary: DashboardSummary?
let transactions: [Transaction]
var body: some View {
ScrollView {
VStack(spacing: 16) {
PeriodPicker(selection: $selectedPeriod)
if let summary {
SummaryCard(title: "Total", value: summary.totalSpent)
SummaryCard(title: "Daily Avg", value: summary.dailyAverage)
ForEach(summary.categoryBreakdown, id: \.category) { item in
CategoryRow(name: item.category, amount: item.amount)
}
Button("Details") { withAnimation { showDetails.toggle() } }
if showDetails {
MerchantList(merchants: summary.topMerchants)
}
} else {
ProgressView()
}
}
}
.task(id: selectedPeriod) {
summary = await computeSummary(
from: transactions,
for: selectedPeriod
)
}
}
}
现在 body 只 读取一个结构体 并将其映射为视图——不再进行过滤、分组或排序。切换 showDetails 立即生效,繁重的计算仅在 selectedPeriod 变化时运行(在主线程之外)。
常见性能技巧
1. 缓存耗时对象
// ❌ New DateFormatter on every body call
Text(DateFormatter.localizedString(from: date,
dateStyle: .long,
timeStyle: .short))
// ✅ Static – created once
private static let formatter: DateFormatter = {
let f = DateFormatter()
// configure f …
return f
}()
Text(Self.formatter.string(from: date))
同样适用于 NumberFormatter、JSONDecoder、正则表达式模式等——任何 昂贵但无状态 的对象。
2. 避免在 body 中直接使用 .sorted() / .filter()
// ❌ Sorts on every evaluation
List(items.sorted(by: { $0.date > $1.date })) { ... }
// ✅ Sort when data changes
@State private var sortedItems: [Item] = []
.onChange(of: items) { _, new in
sortedItems = new.sorted(by: { $0.date > $1.date })
}
如果在 body 中看到这些代码,就是代码异味。应将它们移到 .onChange、.task 或专用的 view model 中,并将结果存入 @State。
3. 保持视图小巧
// ❌ AvatarImage rebuilds when unrelated state changes
struct ProfileView: View {
@State private var isEditing = false
var body: some View {
VStack {
AvatarImage(url: user.avatarURL) // unnecessary rebuild!
Toggle("Edit", isOn: $isEditing)
}
}
}
// ✅ Extract it – now it only rebuilds when `avatarURL` changes
struct AvatarImage: View {
let url: URL
var body: some View {
AsyncImage(url: url)
.frame(width: 80, height: 80)
.clipShape(Circle())
}
}
更小、更独立的视图意味着更少的不必要重新计算。
4. 在 iOS 17+ 中优先使用 @Observable 而非 @ObservableObject
// ❌ With ObservableObject, any @Published change forces a full view refresh
class UserModel: ObservableObject {
@Published var name: String
@Published var bio: String
}
// ✅ @Observable tracks per‑property, so only the views that read a changed
// property are refreshed
@Observable class UserModel {
var name: String
var bio: String
}
细粒度的追踪可以减少需要重绘的 UI 区域。
5. 使用 .task(id:) 进行异步工作
// Runs off the main thread, cancels automatically when `id` changes
.task(id: selectedPeriod) {
summary = await computeSummary(from: transactions,
for: selectedPeriod)
}
这可以把繁重的工作从 body 中移出,并在相关输入变化时自动取消。
TL;DR
body应该保持轻量 – 避免在其中进行大量计算、排序、过滤或对象创建。- 使用
@State、.task、.onChange等在输入变化时缓存或预计算昂贵的结果。 - 将大视图拆分为更小的子视图,这样只有真正需要更新的部分才会重新计算。
- 利用
@Observable(iOS 17+)进行属性级别的变更跟踪。 - 使用
.task(id:)运行异步工作,保持主线程响应。
通过遵循这些模式,让 SwiftUI 的 diff 引擎发挥最大作用——只渲染实际改变的内容。
SwiftUI 性能技巧
“把
body当作每秒被调用 100 次来对待。因为有时确实是这样。”
常见陷阱
// Fires on every body evaluation
let _ = Task { await loadData() }
// ✅ Only runs when `selectedCategory` changes
.task(id: selectedCategory) { await loadData(for: selectedCategory) }
如何诊断
- 打开 Instruments → SwiftUI 模板 → View Body 调用。
- 如果一个视图的
body被调用 200 次,而你期望只有 2 次,那么问题就找到了。
永远不要凭直觉进行优化——让分析器告诉你实际发生了什么。
最佳实践
- 保持
body纯净且轻量:它只应读取预先计算好的状态并映射为视图。 - 避免在
body中进行耗时操作。将任何繁重的工作移到task、onAppear或视图模型中。 - 使用
.task(id:)(或类似的修饰符)仅在其依赖项变化时运行异步工作。
结论
SwiftUI 并不慢;它会惩罚在错误位置进行的耗时工作。
一旦你理解了渲染周期,性能问题就会消失,你可以不再怪罪框架,而是开始交付流畅的应用。
觉得有帮助吗?关注我获取更多 SwiftUI 深度解析,并在评论中分享你的性能“战争”故事!