SwiftUI 性能优化 — 流畅的 UI,减少重新计算

发布: (2025年12月3日 GMT+8 10:36)
5 min read
原文: Dev.to

Source: Dev.to

SwiftUI 是 快速 的——但前提是你要正确使用它。

随着时间的推移,你会遇到:

  • 卡顿的滚动性能
  • 卡顿的动画
  • 列表缓慢
  • 视图刷新过于频繁
  • 不必要的重新计算
  • 异步工作阻塞 UI
  • 图片导致的内存激增
  • 120 Hz 动画掉帧

本指南展示了我在所有应用中遵循的 真实场景性能规则。这些并非理论,而是解决你在构建完整 SwiftUI 应用时实际遇到的问题。让你的 UI 如黄油般顺滑。🧈⚡

1. 将重量级视图拆分为更小的子视图

当任何 @State/@ObservedObject 变化时,SwiftUI 会重新渲染 整个视图

不佳:

VStack {
    Header()
    ExpensiveList(items: items)   // ← 巨大的视图
}
.onChange(of: searchQuery) {
    // 整个视图重新计算
}

更好:

VStack {
    Header()
    ExpensiveList(items: items)   // 已隔离
}

最佳:

struct BigScreen: View {
    var body: some View {
        Header()
        BodyContent()   // 隔离的子视图防止额外的重新计算
    }
}

小而可复用的组件 = 巨大的性能提升。

2. 正确使用 @StateObject 与 @observable

对不应 重新初始化 的对象使用 @StateObject

@StateObject var viewModel = HomeViewModel()

对轻量模型使用 @observable

@Observable
class HomeViewModel {  }

对简单值使用 @State(不要在 @State 中存放复杂结构体)。绝不要@State 中存放重量级对象。

3. 将异步工作放在主线程之外

错误示例:

func load() async {
    let data = try? API.fetch()   // 速度慢
    self.items = data             // UI 冻结
}

正确示例:

func load() async {
    let data = try? await Task.detached { await API.fetch() }.value
    await MainActor.run { self.items = data }
}

永远不要阻塞主线程——SwiftUI 依赖它。

4. 对重量级绘制使用 .drawingGroup()

如果你渲染渐变、模糊层、大符号或复杂遮罩,开销会很快升高。

MyComplexShape()
    .drawingGroup()

这会强制 GPU 渲染通道 → 大幅提升速度。

5. 优化图片(最常见的卡顿来源)

使用已调整大小的缩略图:

Image(uiImage: image.resized(to: 200))

避免在 ScrollView 中加载大图。推荐使用:

  • .resizable().scaledToFit()
  • .interpolation(.medium)
  • 异步加载 + 缓存

对于远程图片,使用 URLCache 或类似 Nuke 的库。

6. 避免在 ScrollView 中进行重量级布局工作

常见错误:

ScrollView {
    ForEach(items) { item in
        ExpensiveLayout(item: item)
    }
}

在滚动时进行重量级布局会导致卡顿。

解决方案:

  • 减少修饰符
  • 缓存计算值
  • 将子视图拆分为隔离的组件

7. 在需要时优先使用 LazyVGrid / LazyVStack 而非 List

List 很棒——但并非在所有情况下都适用。

使用 LazyVStack 的场景:

  • 需要自定义动画
  • 拥有大型组合布局
  • 行内包含复杂容器

使用 List 的场景:

  • 行结构简单
  • 想要原生单元格优化

8. 避免使用 .id(UUID()) 重新计算视图

MyView()
    .id(UUID())   // BAD – 销毁身份

这会在每帧强制完整视图重新加载。仅在需要受控重置时使用 .id(...)

9. 计算属性应保持快速

不佳:

var filtered: [Item] {
    hugeArray.filter {  }   // 开销大
}

每次渲染都会重新计算。

更好:

@State private var filtered: [Item] = []

在需要时更新:

.onChange(of: searchQuery) { _ in
    filtered = hugeArray.filter {  }
}

10. 使用 Transaction 控制动画开销

默认动画可能卡顿。通过以下方式平滑它们:

withTransaction(Transaction(animation: .snappy)) {
    isOpen.toggle()
}

自定义动画事务可以减少布局跳动。

11. 在批量更新期间关闭动画

withAnimation(.none) {
    items = newItems
}

防止在大列表操作时出现卡顿。

12. 使用 Instruments(是的,它对 SwiftUI 有效)

使用以下工具进行分析:

  • SwiftUI “Dirty Views”
  • Memory Graph
  • Time Profiler
  • Allocation Tracking
  • FPS 掉帧

90 % 的卡顿来源于:

  • 巨大视图
  • 昂贵的初始化
  • 图片
  • 主线程阻塞

✔️ 最终性能检查清单

发布前,请确保:

  • 没有主线程 API 调用
  • 没有昂贵的计算属性
  • ScrollView 内没有布局抖动
  • 图片已调整大小或已缓存
  • 重量级视图已拆分为组件
  • ViewModel 使用 @StateObject@observable
  • 动画使用 .snappy.spring 并已隔离
  • 列表在需要时使用惰性容器
Back to Blog

相关文章

阅读更多 »

Bf-Trees:突破页面壁垒

你好,我是Maneshwar。我正在开发FreeDevTools——一个在线的开源中心,将 dev tools、cheat codes 和 TLDR 汇集在一个地方,方便……