mcpField
A
axiom-swiftui-performance

axiom-swiftui-performance

github.com/charleswiltgen开发1.2k215 次安装今天更新
代码仓库

关于 axiom-swiftui-performance

面向 SwiftUI 开发者的性能分析与优化技能,介绍使用 Instruments 26 定位视图更新耗时和不必要刷新的方法,并涵盖相关优化思路。

主要功能

  1. 01使用 SwiftUI Instrument 分析性能
  2. 02查看视图更新的因果关系
  3. 03定位耗时的视图主体更新
  4. 04排查不必要的视图刷新
  5. 05提供视图层级与计算优化建议

适用场景

  1. 01诊断滚动卡顿或动画停顿
  2. 02找出耗时的 SwiftUI 视图更新
  3. 03排查数据未变化却重复刷新的视图
  4. 04优化大型数据集和复杂视图结构

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-swiftui-performance

安装后在 Claude Code 中输入/axiom-swiftui-performance确认已加载。

详细介绍

SwiftUI 性能优化

何时使用此技能

适用于以下情况:

  • App 响应变慢(卡顿、无响应、滚动延迟)
  • 动画在运行期间暂停或跳动
  • 滚动性能不佳
  • 性能分析发现瓶颈在 SwiftUI
  • View 主体运行耗时过长
  • View 更新频率高于必要水平
  • 需要了解 SwiftUI 更新的因果关系

示例提示词

以下是开发者经常提出的真实问题,此技能旨在解答这些问题:

1. “我的 App 滚动卡顿,动画也不流畅。如何判断是不是 SwiftUI 导致的?”

→ 此技能介绍如何使用 Instruments 26 中的新 SwiftUI Instrument,判断瓶颈是在 SwiftUI 还是其他层

2. “我正在使用新的 SwiftUI Instrument,看到橙色/红色的长更新条。如何知道是什么导致的?”

→ 此技能介绍 Cause & Effect Graph 模式,展示数据如何在 App 中流动,以及哪些状态变化会触发耗时的更新

3. “有些 View 的数据明明没变,却频繁更新。如何找出有问题的 View?”

→ 此技能演示如何通过可视化时间线检测不必要的更新并排查 Identity 问题

4. “我有大型数据结构和复杂的 View 层级。如何针对 SwiftUI 性能进行优化?”

→ 此技能介绍性能模式:拆分 View 层级、降低 body 的复杂度,以及使用 @Sendable 优化检查清单

5. “我们有性能优化的截止期限,我需要了解 SwiftUI 中哪些地方很慢。关键指标有哪些?”

→ 此技能提供用于确定优化优先级的决策树,并针对紧迫情况提供专业建议,帮助权衡取舍


概述

核心原则:确保 View 主体快速更新,并且仅在必要时更新,从而获得出色的 SwiftUI 性能。

WWDC 2025 全新内容:Instruments 26 中的新一代 SwiftUI Instrument 提供全面的性能分析功能,包括:

  • 耗时更新的可视化时间线(根据严重程度以橙色/红色标示)
  • Cause & Effect Graph,展示 App 中的数据流
  • 与 Time Profiler 集成,用于 CPU 分析
  • 卡死和卡顿跟踪

主要性能问题:

  1. View 主体更新耗时过长 — View 主体运行耗时过长
  2. 不必要的 View 更新 — 数据实际未变化时 View 仍在更新

iOS 26 框架性能改进

“框架的性能改进让 Apple 全平台的 App 都受益,从我们的 App 到你们的 App。” — WWDC 2025-256

iOS 26 中的 SwiftUI 带来了显著的性能提升,所有 App 都能自动受益。这些改进与新的性能分析工具协同工作,让 SwiftUI 开箱即用地更快。

List 性能(侧重 macOS)
大型列表性能大幅提升
  • macOS 上包含 100,000 项以上的列表加载速度提升 6 倍
  • 大型列表的更新速度提升 16 倍
  • 列表越大,提升幅度也越大
  • 所有平台(iOS、iPadOS、watchOS,而不仅是 macOS)都能受益于这些改进
List(trips) { trip in // 100k+ items
    TripRow(trip: trip)
}
// iOS 26: Loads 6x faster, updates 16x faster on macOS
// All platforms benefit from performance improvements
对 App 的影响
  • 大型数据集(10k 项以上)的性能提升明显
  • 筛选和排序操作更快完成
  • 列表的实时更新响应更快
  • 文件浏览器、联系人列表和数据表等 App 均可受益
滚动性能
高速滚动时减少掉帧

SwiftUI 改进了 iOS 和 macOS 上的用户界面更新调度。这提升了响应速度,也让 SwiftUI 能完成更多工作,为即将显示的帧做好准备。总体而言,这降低了 App 在高帧率下快速滚动时掉帧的概率。

主要改进
  1. 更好的帧调度 — SwiftUI 有更多时间为即将到来的帧做准备
  2. 响应速度提升 — UI 更新的调度效率更高
  3. 减少丢帧 — 尤其是在 120Hz(ProMotion)下快速滚动时
你会注意到的场景
  • 滚动浏览包含大量图片的内容
  • 高帧率设备(iPhone Pro、支持 ProMotion 的 iPad Pro)
  • 包含多个视图的复杂列表行
嵌套 ScrollViews 与 Lazy Stacks
照片轮播和多轴滚动现在可以正确优化
ScrollView(.horizontal) {
    LazyHStack {
        ForEach(photoSets) { photoSet in
            ScrollView(.vertical) {
                LazyVStack {
                    ForEach(photoSet.photos) { photo in
                        PhotoView(photo: photo)
                    }
                }
            }
        }
    }
}
// iOS 26: Nested scrollviews now properly delay loading with lazy stacks
// Great for photo carousels, Netflix-style layouts, multi-axis content

iOS 26 之前 嵌套 ScrollViews 无法正确延迟加载 lazy stack 内容,导致所有嵌套内容立即加载。

iOS 26 之后 嵌套 ScrollViews 中的 lazy stacks 现在会延迟加载,直到内容即将显示,与单层 ScrollViews 的行为一致。

使用场景
  • 水平/垂直滚动的照片图库
  • 类似 Netflix 的分类行
  • 多维数据浏览器
  • 带有垂直详情滚动的图片轮播
SwiftUI 性能工具增强
Instruments 26 中的新轨道

SwiftUI 工具现在包含以下专用轨道:

  1. 长时间 View Body 更新 — 找出开销较大的 body 计算
  2. 平台 View 更新 — 跟踪 UIKit/AppKit 桥接性能(长时间 Representable 更新)
  3. 其他长时间更新 — 所有其他类型的长时间 SwiftUI 操作

下一节将详细介绍这些轨道。

性能改进摘要
自动生效的改进(仅需重新编译)
  • ✅ 列表加载速度提升 6 倍(macOS 上,10 万项以上)
  • ✅ 列表更新速度提升 16 倍(macOS 上)
  • ✅ 减少滚动时的丢帧
  • ✅ 改进 iOS/macOS 上的帧调度
  • ✅ 优化嵌套 ScrollView 的延迟加载

无需更改代码 — 使用 iOS 26 SDK 重新构建即可获得这些改进。

交叉参考 SwiftUI 26 Features(swiftui-26-ref skill)— iOS 26 中所有 SwiftUI 更改的完整指南


SwiftUI 工具(Instruments 26)

开始使用

要求:

  • 安装 Xcode 26
  • 将设备更新到最新的 OS 版本(支持录制 SwiftUI 跟踪数据)
  • 以 Release 模式构建应用,以便准确地进行性能分析

启动:

  1. 在 Xcode 中打开项目
  2. 按 Command-I 进行性能分析
  3. 在模板选择器中选择 SwiftUI template
  4. 点击 Record 按钮
模板内容

SwiftUI 模板包含三个工具:

  1. SwiftUI Instrument(新增)— 识别 SwiftUI 代码中的性能问题
  2. Time Profiler — 显示随时间变化的 CPU 工作采样
  3. Hangs and Hitches — 跟踪应用响应速度
SwiftUI 工具轨道
轨道 1:更新组
  • 显示 SwiftUI 正在执行操作的时段
  • CPU 峰值期间为空? → 问题可能出在 SwiftUI 之外
轨道 2:长时间 View Body 更新
  • 标出 body 属性耗时过长的情况
  • 最常见的性能问题 — 从这里开始排查
轨道 3:长时间 Representable 更新
  • 找出缓慢的 UIViewRepresentable/NSViewRepresentable 更新
  • UIKit/AppKit 集成性能
轨道 4:其他长时间更新
  • 所有其他类型的长时间 SwiftUI 操作
颜色标记系统

根据更新可能导致卡顿的概率,以橙色和红色显示:

  • 红色 — 极有可能导致卡顿/挂起(优先调查)
  • 橙色 — 有一定可能导致问题
  • 灰色 — 正常更新,无需担心

注意:更新是否会实际导致卡顿取决于设备状况,但红色更新的优先级最高。


了解渲染循环

正常帧渲染
Frame 1:
├─ Handle events (touches, key presses)
├─ Update UI (run view bodies)
│  └─ Complete before frame deadline ✅
├─ Hand off to system
└─ System renders → Visible on screen

Frame 2:
├─ Handle events
├─ Update UI
│  └─ Complete before frame deadline ✅
├─ Hand off to system
└─ System renders → Visible on screen

结果:动画平滑流畅

出现卡顿的帧(长时间 View Body)
Frame 1:
├─ Handle events
├─ Update UI
│  └─ ONE VIEW BODY TOO SLOW
│  └─ Runs past frame deadline ❌
├─ Miss deadline
└─ Previous frame stays visible (HITCH)

Frame 2: (Delayed)
├─ Handle events (delayed by 1 frame)
├─ Update UI
├─ Hand off to system
└─ System renders → Finally visible

Result: Previous frame visible for 2+ frames = animation stutter
使用 Hitch 分析帧卡顿(更新过多)
Frame 1:
├─ Handle events
├─ Update UI
│  ├─ Update 1 (fast)
│  ├─ Update 2 (fast)
│  ├─ Update 3 (fast)
│  ├─ ... (100 more fast updates)
│  └─ Total time exceeds deadline ❌
├─ Miss deadline
└─ Previous frame stays visible (HITCH)

结果:许多小更新累积起来,导致错过截止时间

关键见解:视图主体的运行时间很重要,因为错过帧截止时间会造成卡顿,使动画不够流畅。

参考资料:


问题 1:视图主体更新耗时过长

识别耗时较长的更新
  1. 使用 SwiftUI 模板在 Instruments 中录制跟踪记录
  2. 查看 Long View Body Updates 通道——是否有橙色或红色条?
  3. 展开 SwiftUI 跟踪轨道以查看子轨道
  4. 选择 View Body Updates 子轨道
  5. 筛选耗时较长的更新:
    • 详细信息面板 → 下拉菜单 → 选择“Long View Body Updates summary”
使用 Time Profiler 分析

工作流:

  1. 在 Long View Body Updates summary 中找到耗时较长的更新
  2. 将鼠标悬停在视图名称上 → 点击箭头 → “Show Updates”
  3. 右键点击耗时较长的更新 → “Set Inspection Range and Zoom”
  4. 切换到 Time Profiler 工具轨道

你会看到:

  • 视图主体执行期间记录的样本调用堆栈
  • 每帧耗时(最左侧列)
  • 嵌套在深层 SwiftUI 调用堆栈中的视图主体

查找瓶颈:

  1. 按住 Option 并点击,展开主线程调用堆栈
  2. 按 Command-F 搜索视图名称(例如“LandmarkListItemView”)
  3. 在耗时列中找出开销较大的操作
常见的高开销操作
创建格式化器(开销很大)

❌ 错误做法——在视图主体中创建格式化器:

struct LandmarkListItemView: View {
    let landmark: Landmark
    @State private var userLocation: CLLocation

    var distance: String {
        // ❌ Creating formatters every time body runs
        let numberFormatter = NumberFormatter()
        numberFormatter.maximumFractionDigits = 1

        let measurementFormatter = MeasurementFormatter()
        measurementFormatter.numberFormatter = numberFormatter

        let meters = userLocation.distance(from: landmark.location)
        let measurement = Measurement(value: meters, unit: UnitLength.meters)
        return measurementFormatter.string(from: measurement)
    }

    var body: some View {
        HStack {
            Text(landmark.name)
            Text(distance) // Calls expensive distance property
        }
    }
}

速度慢的原因:

  • 创建格式化器的开销很大(每个需要几毫秒)
  • 每次运行视图主体时都会创建
  • 在主线程上运行 → 应用必须等待才能继续更新 UI
  • 视图越多 → 累积耗时越长

✅ 正确做法——集中缓存格式化器:

@Observable
class LocationFinder {
    private let formatter: MeasurementFormatter
    private let landmarks: [Landmark]
    private var distanceCache: [Landmark.ID: String] = [:]

    init(landmarks: [Landmark]) {
        self.landmarks = landmarks

        // Create formatters ONCE during initialization
        let numberFormatter = NumberFormatter()
        numberFormatter.maximumFractionDigits = 1

        self.formatter = MeasurementFormatter()
        self.formatter.numberFormatter = numberFormatter

        updateDistances()
    }

    func didUpdateLocations(_ locations: [CLLocation]) {
        guard let location = locations.last else { return }
        updateDistances(from: location)
    }

    private func updateDistances(from location: CLLocation? = nil) {
        guard let location else { return }

        for landmark in landmarks {
            let meters = location.distance(from: landmark.location)
            let measurement = Measurement(value: meters, unit: UnitLength.meters)
            distanceCache[landmark.id] = formatter.string(from: measurement)
        }
    }

    func distanceString(for landmarkID: Landmark.ID) -> String {
        distanceCache[landmarkID] ?? "Unknown"
    }
}

struct LandmarkListItemView: View {
    let landmark: Landmark
    @Environment(LocationFinder.self) private var locationFinder

    var body: some View {
        HStack {
            Text(landmark.name)
            Text(locationFinder.distanceString(for: landmark.id)) // ✅ Fast lookup
        }
    }
}

好处:

  • 格式化器只创建一次,可供所有地标重复使用
  • 位置变化时预先计算字符串
  • 视图主体只需读取缓存值(即时完成)
  • 消除了耗时较长的视图主体更新
其他高开销操作

复杂计算:

// ❌ Don't calculate in view body
var body: some View {
    let result = expensiveAlgorithm(data) // Complex math, sorting, etc.
    Text("\(result)")
}

// ✅ Calculate in model, cache result
@Observable
class ViewModel {
    private(set) var result: Int = 0

    func updateData(_ data: [Int]) {
        result = expensiveAlgorithm(data) // Calculate once
    }
}

网络/文件 I/O:

// ❌ NEVER do I/O in view body
var body: some View {
    let data = try? Data(contentsOf: fileURL) // ❌ Synchronous I/O
    // ...
}

// ✅ Load asynchronously, store in state
@State private var data: Data?

var body: some View {
    // Just read state
}
.task {
    data = try? await loadData() // Async loading
}

图像处理:

// ❌ Don't process images in view body
var body: some View {
    let thumbnail = image.resized(to: CGSize(width: 100, height: 100))
    Image(uiImage: thumbnail)
}

// ✅ Process images in background, cache
.task {
    await processThumbnails()
}
验证修复效果

实施修复后:

  1. 在 Instruments 中录制新的跟踪记录
  2. 检查 Long View Body Updates summary
  3. 确认列表中不再出现你的视图(或其数量显著减少)

注意:应用启动时的更新可能仍然耗时较长(因为需要构建初始视图层级结构)——这是正常的,不会导致滚动时卡顿。


问题 2:不必要的视图更新

不必要的更新为何重要

即使单次更新很快,更新过多也会累积影响:

100 fast updates × 2ms each = 200ms total
→ Misses 16.67ms frame deadline
→ Hitch
识别不必要的更新

场景:点按某个项目上的收藏按钮,会更新列表中的所有项目。

预期结果:只有被点按的项目会更新。 实际结果:所有可见项目都会更新。

查找方法:

  1. 录制跟踪记录时,留意用户交互
  2. 高亮时间线中的相关部分
  3. 在详细信息面板中展开层级结构
  4. 统计更新次数——是否超出预期?
了解 SwiftUI 的数据模型

SwiftUI 使用 AttributeGraph 定义依赖关系,避免不必要地重新运行视图。

属性与依赖关系
struct OnOffView: View {
    @State private var isOn: Bool = false

    var body: some View {
        Text(isOn ? "On" : "Off")
    }
}

SwiftUI 创建的内容:

  1. 视图属性——存储视图结构体(会频繁重新创建)
  2. 状态存储——保存 isOn 的值(在整个视图生命周期内保持)
  3. 信号属性——跟踪状态何时发生变化
  4. 视图主体属性——依赖状态信号
  5. 文本属性——依赖视图主体

状态发生变化时:

  1. 创建事务(计划在下一帧执行的更改)
  2. 将信号属性标记为过期
  3. 遍历依赖链,将依赖属性标记为过期(只需设置标记,速度很快)
  4. 渲染前,更新所有过期属性
  5. 再次运行视图主体,生成新的 Text 结构体
  6. 继续更新,直到所有所需属性都已更新
  7. 渲染帧
因果关系图

用途:直观展示是什么让你的视图主体标记为过时。

示例图:

[Gesture] → [State Change] → [View Body Update]
               ↓
         [Other View Bodies]

节点类型:

  • 蓝色节点 — 你的代码或操作(手势、状态变化、视图主体)
  • 系统节点 — SwiftUI/系统工作
  • 标记为 "update" 的箭头 — 导致更新
  • 标记为 "creation" 的箭头 — 导致视图出现

选择节点:

  • 点击状态变化节点 → 查看值在何处更新的回溯信息
  • 点击视图主体节点 → 查看哪些视图更新了以及原因

访问图表:

  1. 详细信息窗格 → 展开层级以查找视图
  2. 将鼠标悬停在视图名称上 → 点击箭头
  3. 选择**"显示因果关系图"**
示例:收藏列表问题

问题:

@Observable
class ModelData {
    var favoritesCollection: Collection // Contains array of favorites

    func isFavorite(_ landmark: Landmark) -> Bool {
        favoritesCollection.landmarks.contains(landmark) // ❌ Depends on whole array
    }
}

struct LandmarkListItemView: View {
    let landmark: Landmark
    @Environment(ModelData.self) private var modelData

    var body: some View {
        HStack {
            Text(landmark.name)
            Button {
                modelData.toggleFavorite(landmark) // Modifies array
            } label: {
                Image(systemName: modelData.isFavorite(landmark) ? "heart.fill" : "heart")
            }
        }
    }
}

发生的情况:

  1. 每个视图都会调用 isFavorite(),访问 favoritesCollection.landmarks 数组
  2. @Observable 创建依赖关系:每个视图都依赖整个数组
  3. 点击按钮会调用 toggleFavorite(),修改数组
  4. 所有视图都被标记为过时(数组已更改)
  5. 所有视图主体都会运行(即使只有一个发生了变化)

因果关系图显示:

[Gesture] → [favoritesCollection.landmarks array change] → [All LandmarkListItemViews update]

✅ 解决方案 — 细粒度依赖:

@Observable
class LandmarkViewModel {
    var isFavorite: Bool = false

    func toggleFavorite() {
        isFavorite.toggle()
    }
}

@Observable
class ModelData {
    private(set) var viewModels: [Landmark.ID: LandmarkViewModel] = [:]

    init(landmarks: [Landmark]) {
        for landmark in landmarks {
            viewModels[landmark.id] = LandmarkViewModel()
        }
    }

    func viewModel(for landmarkID: Landmark.ID) -> LandmarkViewModel? {
        viewModels[landmarkID]
    }
}

struct LandmarkListItemView: View {
    let landmark: Landmark
    @Environment(ModelData.self) private var modelData

    var body: some View {
        if let viewModel = modelData.viewModel(for: landmark.id) {
            HStack {
                Text(landmark.name)
                Button {
                    viewModel.toggleFavorite() // ✅ Only modifies this view model
                } label: {
                    Image(systemName: viewModel.isFavorite ? "heart.fill" : "heart")
                }
            }
        }
    }
}

结果:

  • 每个视图只依赖自己的视图模型
  • 点击按钮只会更新该视图模型
  • 只有一个视图主体会运行

因果关系图显示:

[Gesture] → [Single LandmarkViewModel change] → [Single LandmarkListItemView update]

环境更新

环境的工作方式
struct EnvironmentValues {
    // Dictionary-like value type
    var colorScheme: ColorScheme
    var locale: Locale
    // ... many more values
}

每个视图都通过 @Environment 属性包装器依赖整个 EnvironmentValues 结构体。

环境更改时会发生什么
  1. 任何环境值发生变化(例如,启用深色模式)
  2. 所有依赖 @Environment 的视图都会收到通知
  3. 每个视图都会检查自己读取的特定值是否发生变化
  4. 如果值发生变化 → 视图主体会运行
  5. 如果值没有变化 → SwiftUI 会跳过运行视图主体(视图已是最新状态)

开销:即使主体没有运行,检查更新仍会产生开销。

图表中的环境更新节点

两种类型:

  1. 外部环境 — 来自 SwiftUI 之外的应用级更改(色彩方案、辅助功能设置)
  2. EnvironmentWriter — 通过 .environment() 修饰符在 SwiftUI 内部进行的更改

示例:

View1 reads colorScheme:
[External Environment] → [View1 body runs] ✅

View2 reads locale (doesn't read colorScheme):
[External Environment] → [View2 body check] (body doesn't run - dimmed icon)

同一更新会显示为多个节点:将鼠标悬停在任意节点上,或点击任意节点,即可查看同一更新 → 所有相关节点会一起高亮显示。

环境更新性能警告

⚠️ 避免在环境中存储频繁变化的值:

// ❌ DON'T DO THIS
struct ContentView: View {
    @State private var scrollOffset: CGFloat = 0

    var body: some View {
        ScrollView {
            // Content
        }
        .environment(\.scrollOffset, scrollOffset) // ❌ Updates on every scroll frame
        .onPreferenceChange(ScrollOffsetKey.self) { offset in
            scrollOffset = offset
        }
    }
}

不推荐的原因:

  • 环境更改会触发所有子视图进行检查
  • 滚动 = 每秒 60 次以上更新
  • 会严重影响性能

✅ 更好的方法:

// Pass via parameter or @Observable model
struct ContentView: View {
    @State private var scrollViewModel = ScrollViewModel()

    var body: some View {
        ScrollView {
            ChildView(scrollViewModel: scrollViewModel) // Direct parameter
        }
    }
}

环境适用于:

  • 色彩方案
  • 区域设置
  • 辅助功能设置
  • 其他相对稳定的值

性能优化检查清单

性能分析前
  • 以 Release 模式构建(Debug 模式会产生开销)
  • 在真实设备上测试(模拟器性能 ≠ 真实设备性能)
  • 将设备更新到最新操作系统(以支持 SwiftUI 跟踪)
  • 确定需要进行性能分析的具体卡顿交互
性能分析期间
  • 在 Instruments 26 中使用 SwiftUI 模板
  • 首先关注 Long View Body Updates 通道
  • 检查 Update Groups 通道(为空 = 问题出在 SwiftUI 之外)
  • 记录符合实际情况的用户工作流(而非人为设计的场景)
  • 保持性能分析会话简短(更易于分析)
分析耗时较长的视图主体更新
  • 将详细信息窗格筛选为 "Long View Body Updates"
  • 先查看红色更新,再查看橙色更新
  • 使用 Time Profiler 查找耗时操作
  • 查找格式化器创建、计算、I/O 操作
  • 检查是否可以将工作移至模型层
分析不必要的更新
  • 统计视图主体更新次数——是否超出预期?
  • 使用因果关系图追踪数据流
  • 检查是否依赖整个数组/集合
  • 确认每个视图只依赖相关数据
  • 避免使用频繁变化的环境值
优化后
  • 录制新的跟踪记录,验证改进效果
  • 比较优化前后的 Long View Body Updates 计数
  • 在支持的最慢设备上测试
  • 在真实使用场景中监控
  • 在开发过程中定期分析性能

生产环境压力:性能问题影响线上环境时

问题

生产环境出现性能问题时,你会面临相互冲突的压力:

  • 工程经理:“尽快修复”
  • 产品副总裁:“用户已经抱怨好几个小时了”
  • 部署窗口:距离下一个 App Store 审核窗口还有 6 小时
  • 诱惑:快速修复(添加 .compositingGroup()、禁用动画、简化视图)

问题在于:凭猜测进行快速修复,80% 的时候会失败,还会浪费部署窗口。

危险信号——抵制这些施压手段

如果你在截止日期压力下听到以下任何说法,停下来并使用 SwiftUI Instrument:

  • ❌ “直接添加 .compositingGroup()”——不进行性能分析,你就不知道这是否有帮助
  • ❌ “如果不行,我们可以回滚”——App Store 审核需要 24 小时;回滚并不快
  • ❌ “其他应用也使用这种模式”——不代表它能解决你的具体问题
  • ❌ “用户暂时可以接受性能下降”——一旦发布,你就要等 24 小时
  • ❌ “我们没时间做性能分析”——猜错的话,你会更没时间
一次 SwiftUI Instrument 录制(30 分钟流程)

面临生产环境压力时,一次有效的诊断录制胜过随机修复:

时间预算:

  • 以 Release 模式构建:5 分钟
  • 启动并交互,以触发卡顿:3 分钟
  • 录制 SwiftUI Instrument 跟踪记录:5 分钟
  • 查看 Long View Body Updates 时间轨道:5 分钟
  • 检查 Cause & Effect Graph:5 分钟
  • 找出具体的高开销视图:2 分钟

总计:25 分钟即可准确了解卡在哪里

然后:

  • 应用有针对性的修复(15-30 分钟)
  • 在 Instruments 中再次测试(5 分钟)
  • 满怀信心地发布

总耗时:诊断和修复共 1 小时 15 分钟,还能留出 4 小时以上测试边缘情况。

时间成本对比
方案 A:猜测并祈祷
  • 实施耗时:30 分钟
  • 部署耗时:20 分钟
  • 确认失败所需时间:24 小时(下一个 App Store 审核)
  • 总延迟:至少 24 小时
  • 用户受影响时间:持续整个部署窗口
方案 B:录制一次 SwiftUI Instrument
  • 诊断耗时:25 分钟
  • 应用有针对性的修复耗时:20 分钟
  • 验证耗时:5 分钟
  • 部署耗时:20 分钟
  • 总耗时:1.5 小时
  • 用户受影响时间:2 小时后停止,而非持续 26 小时以上

判断错误的时间成本:

  • A:延迟 24 小时 + 声誉受损 + 用户持续受影响
  • B:1.5 小时 + 找到实际问题 + 对修复有信心
真实案例:标签页切换卡顿

压力情景:

  • iOS 26 版本已发布
  • 用户反映“标签页切换卡顿”
  • 产品副总裁每小时都在询问进展
  • 距离部署窗口关闭还有 6 小时

糟糕的做法(方案 A):

Junior suggests: "Add .compositingGroup() to TabView"
You: "Sure, let's try it"
Result: Ships without profiling
Outcome: Doesn't fix issue (compositing wasn't the problem)
Next: 24 hours until next deploy window
VP update: "Users still complaining"

好的做法(方案 B):

"Running one SwiftUI Instrument recording of tab transition"
[25 minutes later]
"SwiftUI Instrument shows Long View Body Updates in ProductGridView during transition.
Cause & Effect Graph shows ProductList rebuilding entire grid unnecessarily.
Applying view identity fix (`.id()`) to prevent unnecessary updates"
[30 minutes to implement and test]
"Deployed at 1.5 hours. Verified with Instruments. Tab transitions now smooth."
何时接受压力(同时确保判断正确)

有时,经理要求加快进度是合理的。满足以下条件时,接受压力:

  • 已进行一次 SwiftUI Instrument 录制(25 分钟)
  • 知道具体是哪个视图或操作开销较大
  • 有针对性的修复方案,而非猜测
  • 已在发布前通过 Instruments 验证修复
  • 随版本一起发布的是性能分析数据,而不是对修复效果的期望

记录你的决策:

Slack to VP + team:

"Completed diagnostic: ProductGridView rebuilding unnecessarily during
tab transitions (confirmed in SwiftUI Instrument, Long View Body Updates).
Applied view identity fix. Verified in Instruments - transitions now 16.67ms.
Deploying now."

这能说明:

  • 你做了诊断(而非猜测)
  • 你解决了正确的问题
  • 你验证了修复
  • 你有信心地发布
如果性能分析后问题仍未解决

坦诚说明:

"SwiftUI Instrument showed ProductGridView was the bottleneck.
Applied view identity fix, but performance didn't improve as expected.
Root cause is deeper than expected. Requiring architectural change.
Shipping animation disable (.animation(nil) on TabView) as mitigation.
Proper fix queued for next release cycle."

这不同于盲目猜测:

  • 你有证据证明根本原因
  • 你理解快速修复为何无效
  • 你在用已知的缓解措施争取时间
  • 你承诺在下一个周期完成适当的修复
压力下的决策框架
发布任何修复之前
问题答案为“是”?操作
你是否运行过 SwiftUI Instrument?否停止——进行 25 分钟诊断
你知道哪个视图开销最大吗?否停止——查看因果关系图
你能用一句话解释修复为何有效吗?否停止——你在猜测
你是否已在 Instruments 中验证修复?否停止——发布前先测试
你是否考虑过更简单的解释?否停止——先查阅文档

五项全部回答“是” → 放心发布


常见模式与解决方案

模式 1:列表项依赖关系

问题:更新一个项会更新整个列表

解决方案:使用具有细粒度依赖关系的独立项视图模型

// ❌ Shared dependency
@Observable
class ListViewModel {
    var items: [Item] // All views depend on whole array
}

// ✅ Granular dependencies
@Observable
class ListViewModel {
    private(set) var itemViewModels: [Item.ID: ItemViewModel]
}

@Observable
class ItemViewModel {
    var item: Item // Each view depends only on its item
}
模式 2:视图主体中的计算属性

问题:每次渲染都会执行开销较大的计算

解决方案:移至模型中,并缓存结果

// ❌ Compute in view
struct MyView: View {
    let data: [Int]

    var body: some View {
        Text("\(data.sorted().last ?? 0)") // Sorts every render
    }
}

// ✅ Compute in model
@Observable
class ViewModel {
    var data: [Int] {
        didSet {
            maxValue = data.max() ?? 0 // Compute once when data changes
        }
    }
    private(set) var maxValue: Int = 0
}

struct MyView: View {
    @Environment(ViewModel.self) private var viewModel

    var body: some View {
        Text("\(viewModel.maxValue)") // Just read cached value
    }
}
模式 3:复用格式化器

问题:反复创建格式化器

解决方案:创建一次,重复使用

// ❌ Create every time
var body: some View {
    let formatter = DateFormatter()
    formatter.dateStyle = .short
    Text(formatter.string(from: date))
}

// ✅ Reuse formatter
class Formatters {
    static let shortDate: DateFormatter = {
        let f = DateFormatter()
        f.dateStyle = .short
        return f
    }()
}

var body: some View {
    Text(Formatters.shortDate.string(from: date))
}
模式 4:仅将环境用于稳定值

问题:环境值变化过快

解决方案:使用直接参数或模型

// ❌ Frequently changing in environment
.environment(\.scrollPosition, scrollPosition) // 60+ updates/second

// ✅ Direct parameter or model
ChildView(scrollPosition: scrollPosition)

iOS 26 性能改进

使用 Xcode 26 构建时的自动改进(无需更改代码):

列表
  • 更新速度最高可提升 16 倍
  • macOS 上的大型列表加载速度提升 6 倍
SwiftUI Instrument
  • 新一代性能分析
  • 捕获详细的因果关系信息
  • 帮助你更轻松地了解视图何时更新以及更新原因

调试性能问题

分步流程
  1. 重现问题——找出具体的缓慢交互
  2. 使用 Instruments 进行性能分析——选择 SwiftUI 模板
  3. 检查 Update Groups 通道——SwiftUI 是否在卡顿时执行工作?
  4. 确定问题类型:
    • 视图主体更新耗时过长?→ 查看“耗时过长的更新”部分
    • 更新次数过多?→ 查看“不必要的更新”部分
  5. 对耗时过长的更新使用 Time Profiler(找出开销较大的操作)
  6. 对不必要的更新使用因果关系图(找出依赖关系问题)
  7. 实施修复
  8. 使用新的跟踪记录进行验证
SwiftUI 并非问题所在时
性能问题出现时,Update Groups 通道为空?

问题可能出在其他地方:

  • 网络请求
  • 后台处理
  • 图像加载
  • 数据库查询
  • 第三方框架

后续步骤:


实际影响

示例:Landmarks 应用(摘自 WWDC 2025)

优化前:

  • 每次点击收藏按钮,都会更新所有可见的地标视图
  • 每个视图都会重新创建用于计算距离的格式化器
  • 滚动体验卡顿

优化后:

  • 只有被点击的视图会更新(细粒度视图模型)
  • 格式化器只创建一次,字符串会被缓存
  • 滚动流畅,达到 60 fps

改进:

  • 从 100 多次不必要的视图更新降至每次操作 1 次更新
  • 每个视图节省数毫秒 × 数十个视图 = 显著提升
  • 完全消除了耗时过长的视图主体更新

资源

WWDC:2025-306

文档:/xcode/understanding-hitches-in-your-app、/xcode/analyzing-hangs-in-your-app、/xcode/optimizing-your-app-s-performance

技能:axiom-swiftui-debugging-diag、axiom-swiftui-debugging、axiom-memory-debugging、axiom-xcode-debugging


关键要点

  1. 保持视图主体执行快速 — 让 SwiftUI 有时间及时将界面显示在屏幕上
  2. 仅在需要时更新 — 设计数据流,使视图仅在必要时更新
  3. 谨慎处理环境值 — 不要存储频繁变化的值
  4. 尽早并经常进行性能分析 — 在开发过程中使用 Instruments,而不只是等到出现问题时
  5. 最重要的要点:确保视图主体能够快速且仅在需要时更新,从而实现出色的 SwiftUI 性能

Xcode: 26+ 平台: iOS 26+、iPadOS 26+、macOS Tahoe+、axiom-visionOS 3+ 历史记录: 查看 git log 了解更改

评论

支持 Markdown