SwiftUI 并不慢——是你的代码慢

发布: (2026年2月2日 GMT+8 00:53)
7 min read
原文: Dev.to

Source: Dev.to

SwiftUI 渲染:哪些快,哪些慢

SwiftUI 的渲染引擎本身很快。让它变慢的是你让它 每秒重新执行数百次 的工作。

SwiftUI 的工作原理

SwiftUI 没有一次性触发的 viewDidLoad。它使用 声明式差分系统

  1. 状态变化 – 你更新 @State@Binding@ObservedObject 或其他任何真相源。
  2. 调用 body – SwiftUI 为每个依赖该状态的视图重新评估 body
  3. 进行差分 – SwiftUI 将新的视图树与之前的视图树进行比较。
  4. 仅渲染差异 – 如果没有变化,就不会重新绘制。

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))

同样适用于 NumberFormatterJSONDecoder、正则表达式模式等——任何 昂贵但无状态 的对象。

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) }

如何诊断

  1. 打开 Instruments → SwiftUI 模板 → View Body 调用
  2. 如果一个视图的 body 被调用 200 次,而你期望只有 2 次,那么问题就找到了。

永远不要凭直觉进行优化——让分析器告诉你实际发生了什么。

最佳实践

  • 保持 body 纯净且轻量:它只应读取预先计算好的状态并映射为视图。
  • 避免在 body 中进行耗时操作。将任何繁重的工作移到 taskonAppear 或视图模型中。
  • 使用 .task(id:)(或类似的修饰符)仅在其依赖项变化时运行异步工作。

结论

SwiftUI 并不慢;它会惩罚在错误位置进行的耗时工作。
一旦你理解了渲染周期,性能问题就会消失,你可以不再怪罪框架,而是开始交付流畅的应用。


觉得有帮助吗?关注我获取更多 SwiftUI 深度解析,并在评论中分享你的性能“战争”故事!

Back to Blog

相关文章

阅读更多 »

[SUI] 搜索栏

NavigationStack 中的搜索栏 NavigationStack 可以通过 `searchable` 修饰符添加搜索栏。它的签名是:swift searchable t...

2026 年如何构建 SwiftUI 项目

问题 Xcode 只给你 ContentView.swift,仅此而已。随着你的应用增长,你会出现以下情况:- 一个文件夹里有 50 个文件 - ViewModel 与 View 混在一起 - 没有 cl...

SwiftUI 暗模式:完整实现指南

检测当前颜色方案 SwiftUI 提供 `@Environment.colorScheme` 来检测应用是处于浅色模式还是深色模式。 ```swift struct ContentView: View ```