axiom-swiftui-performance by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-swiftui-performance在以下情况时使用:
以下是开发者实际会问到的、此技能旨在回答的问题:
→ 此技能展示如何使用 Instruments 26 中的新 SwiftUI Instrument 来识别瓶颈是否在 SwiftUI 还是其他层面
→ 此技能涵盖因果关系图模式,展示数据在应用中的流动以及哪些状态变化触发了昂贵的更新
→ 此技能演示如何通过可视化时间线进行不必要的更新检测和身份标识故障排除
→ 此技能涵盖性能模式:分解视图层次结构、最小化视图体复杂性以及使用 @Sendable 优化检查清单
→ 此技能提供用于确定优化优先级的决策树,并通过专业指导理解权衡取舍的压力场景
核心原则:确保你的视图体更新快速且仅在需要时更新,以实现出色的 SwiftUI 性能。
WWDC 2025 新增功能:Instruments 26 中的下一代 SwiftUI instrument 提供全面的性能分析,包括:
关键性能问题:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
"框架的性能改进使所有 Apple 平台上的应用受益,从我们的应用到你的应用。" — WWDC 2025-256
iOS 26 中的 SwiftUI 包含重大的性能提升,所有应用都会自动受益。这些改进与新的性能分析工具协同工作,使 SwiftUI 开箱即用更快。
List(trips) { trip in // 100k+ 项
TripRow(trip: trip)
}
// iOS 26:在 macOS 上加载速度提升 6 倍,更新速度提升 16 倍
// 所有平台都受益于性能改进
SwiftUI 改进了 iOS 和 macOS 上用户界面更新的调度。这提高了响应能力,并让 SwiftUI 有更多时间为即将到来的帧做准备。总而言之,它降低了你的应用在高速滚动和高帧率下掉帧的可能性。
ScrollView(.horizontal) {
LazyHStack {
ForEach(photoSets) { photoSet in
ScrollView(.vertical) {
LazyVStack {
ForEach(photoSet.photos) { photo in
PhotoView(photo: photo)
}
}
}
}
}
}
// iOS 26:嵌套的 ScrollView 现在能正确延迟加载 lazy stack 内容
// 适用于照片轮播、Netflix 风格的布局、多轴内容
iOS 26 之前:嵌套的 ScrollView 不能正确延迟加载 lazy stack 内容,导致所有嵌套内容立即加载。
iOS 26 之后:嵌套 ScrollView 内的 Lazy stacks 现在会延迟加载,直到内容即将出现,与单层 ScrollView 的行为一致。
SwiftUI instrument 现在包含以下专用通道:
这些通道将在下一节详细介绍。
无需更改代码 — 使用 iOS 26 SDK 重新构建即可获得这些改进。
交叉参考 SwiftUI 26 功能(swiftui-26-ref 技能) — iOS 26 SwiftUI 所有变更的全面指南
要求:
启动:
SwiftUI 模板包含三个 instrument:
body 属性耗时过长的情况更新根据可能导致卡顿的可能性以橙色和红色显示:
注意:更新是否实际导致卡顿取决于设备条件,但红色更新是最高优先级。
Frame 1:
├─ 处理事件(触摸、按键)
├─ 更新 UI(运行视图体)
│ └─ 在帧截止时间前完成 ✅
├─ 移交系统
└─ 系统渲染 → 屏幕上可见
Frame 2:
├─ 处理事件
├─ 更新 UI
│ └─ 在帧截止时间前完成 ✅
├─ 移交系统
└─ 系统渲染 → 屏幕上可见
结果:流畅、顺滑的动画
Frame 1:
├─ 处理事件
├─ 更新 UI
│ └─ 一个视图体太慢
│ └─ 运行超过帧截止时间 ❌
├─ 错过截止时间
└─ 前一帧保持可见(卡顿)
Frame 2:(延迟)
├─ 处理事件(延迟 1 帧)
├─ 更新 UI
├─ 移交系统
└─ 系统渲染 → 最终可见
结果:前一帧可见超过 2 帧 = 动画卡顿
Frame 1:
├─ 处理事件
├─ 更新 UI
│ ├─ 更新 1(快)
│ ├─ 更新 2(快)
│ ├─ 更新 3(快)
│ ├─ ...(还有 100 多个快速更新)
│ └─ 总时间超过截止时间 ❌
├─ 错过截止时间
└─ 前一帧保持可见(卡顿)
结果:许多小更新累积导致错过截止时间
关键洞察:视图体运行时间很重要,因为错过帧截止时间会导致卡顿,使动画不够流畅。
参考:
工作流程:
你将看到:
找到瓶颈:
❌ 错误 - 在视图体中创建格式化器:
struct LandmarkListItemView: View {
let landmark: Landmark
@State private var userLocation: CLLocation
var distance: String {
// ❌ 每次视图体运行时都创建格式化器
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) // 调用昂贵的 distance 属性
}
}
}
为什么慢:
✅ 正确 - 集中缓存格式化器:
@Observable
class LocationFinder {
private let formatter: MeasurementFormatter
private let landmarks: [Landmark]
private var distanceCache: [Landmark.ID: String] = [:]
init(landmarks: [Landmark]) {
self.landmarks = landmarks
// 在初始化期间创建格式化器(仅一次)
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)) // ✅ 快速查找
}
}
}
好处:
复杂计算:
// ❌ 不要在视图体中计算
var body: some View {
let result = expensiveAlgorithm(data) // 复杂的数学运算、排序等
Text("\(result)")
}
// ✅ 在模型中计算,缓存结果
@Observable
class ViewModel {
private(set) var result: Int = 0
func updateData(_ data: [Int]) {
result = expensiveAlgorithm(data) // 计算一次
}
}
网络/文件 I/O:
// ❌ 绝不要在视图体中执行 I/O
var body: some View {
let data = try? Data(contentsOf: fileURL) // ❌ 同步 I/O
// ...
}
// ✅ 异步加载,存储在状态中
@State private var data: Data?
var body: some View {
// 仅读取状态
}
.task {
data = try? await loadData() // 异步加载
}
图像处理:
// ❌ 不要在视图体中处理图像
var body: some View {
let thumbnail = image.resized(to: CGSize(width: 100, height: 100))
Image(uiImage: thumbnail)
}
// ✅ 在后台处理图像,缓存
.task {
await processThumbnails()
}
实施修复后:
注意:应用启动时的更新可能仍然很长(构建初始视图层次结构)— 这是正常的,不会在滚动期间导致卡顿。
即使单个更新很快,太多更新也会累积:
100 个快速更新 × 每个 2ms = 总计 200ms
→ 错过 16.67ms 的帧截止时间
→ 卡顿
场景:点击一个项目的收藏按钮会更新列表中的所有项目。
预期:仅被点击的项目更新。实际:所有可见项目都更新。
如何查找:
SwiftUI 使用 AttributeGraph 来定义依赖关系,避免不必要地重新运行视图。
struct OnOffView: View {
@State private var isOn: Bool = false
var body: some View {
Text(isOn ? "On" : "Off")
}
}
SwiftUI 创建的内容:
isOn 值(在整个视图生命周期中持续存在)状态变化时:
目的:可视化是什么将你的视图体标记为过时。
示例图:
[手势] → [状态变更] → [视图体更新]
↓
[其他视图体]
节点类型:
选择节点:
访问图:
问题:
@Observable
class ModelData {
var favoritesCollection: Collection // 包含收藏数组
func isFavorite(_ landmark: Landmark) -> Bool {
favoritesCollection.landmarks.contains(landmark) // ❌ 依赖于整个数组
}
}
struct LandmarkListItemView: View {
let landmark: Landmark
@Environment(ModelData.self) private var modelData
var body: some View {
HStack {
Text(landmark.name)
Button {
modelData.toggleFavorite(landmark) // 修改数组
} label: {
Image(systemName: modelData.isFavorite(landmark) ? "heart.fill" : "heart")
}
}
}
}
发生的情况:
isFavorite(),访问 favoritesCollection.landmarks 数组@Observable 创建依赖关系:每个视图都依赖于整个数组toggleFavorite(),修改数组因果关系图显示:
[手势] → [favoritesCollection.landmarks 数组变更] → [所有 LandmarkListItemViews 更新]
✅ 解决方案 — 细粒度依赖关系:
@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() // ✅ 仅修改此视图模型
} label: {
Image(systemName: viewModel.isFavorite ? "heart.fill" : "heart")
}
}
}
}
}
结果:
因果关系图显示:
[手势] → [单个 LandmarkViewModel 变更] → [单个 LandmarkListItemView 更新]
struct EnvironmentValues {
// 类似字典的值类型
var colorScheme: ColorScheme
var locale: Locale
// ... 更多值
}
每个视图通过 @Environment 属性包装器依赖于整个 EnvironmentValues 结构体。
@Environment 依赖关系的视图收到通知成本:即使视图体不运行,仍然存在检查更新的成本。
两种类型:
.environment() 修饰符在 SwiftUI 内部进行的变更示例:
View1 读取 colorScheme:
[External Environment] → [View1 视图体运行] ✅
View2 读取 locale(不读取 colorScheme):
[External Environment] → [View2 视图体检查](视图体不运行 - 图标变暗)
同一更新显示为多个节点:悬停/点击任何相同更新的节点 → 所有节点一起高亮。
⚠️ 避免在环境中存储频繁变化的值:
// ❌ 不要这样做
struct ContentView: View {
@State private var scrollOffset: CGFloat = 0
var body: some View {
ScrollView {
// 内容
}
.environment(\.scrollOffset, scrollOffset) // ❌ 每滚动一帧都更新
.onPreferenceChange(ScrollOffsetKey.self) { offset in
scrollOffset = offset
}
}
}
为什么不好:
✅ 更好的方法:
// 通过参数或 @Observable 模型传递
struct ContentView: View {
@State private var scrollViewModel = ScrollViewModel()
var body: some View {
ScrollView {
ChildView(scrollViewModel: scrollViewModel) // 直接参数
}
}
}
环境适用于:
当性能问题在生产环境中出现时,你面临相互竞争的压力:
.compositingGroup()、禁用动画、简化视图)问题:基于猜测的快速修复 80% 会失败,并浪费你的部署窗口。
如果你在截止日期压力下听到以下任何说法,请停止并使用 SwiftUI Instrument:
在生产压力下,一次良好的诊断性录制胜过随机修复:
时间预算:
总计:25 分钟即可确切知道什么慢
然后:
总时间:1 小时 15 分钟用于诊断 + 修复,留下 4+ 小时用于边缘情况测试。
出错的时间成本:
压力场景:
错误方法(选项 A):
初级工程师建议:"在 TabView 上添加 .compositingGroup()"
你:"好的,试试看"
结果:未经性能分析就发布
结果:没有解决问题(合成不是问题所在)
下一步:距离下一个部署窗口还有 24 小时
副总裁更新:"用户仍在抱怨"
正确方法(选项 B):
"正在录制一次标签页切换的 SwiftUI Instrument 跟踪"
[25 分钟后]
"SwiftUI Instrument 显示切换期间 ProductGridView 有 Long View Body Updates。
因果关系图显示 ProductList 不必要地重建了整个网格。
应用视图身份修复(`.id()`)以防止不必要的更新"
[30 分钟实施和测试]
"1.5 小时后部署。在 Instruments 中验证。标签页切换现在流畅。"
有时管理者推动速度是正确的。如果满足以下条件,可以接受压力:
记录你的决策:
Slack 给副总裁 + 团队:
"已完成诊断:ProductGridView 在标签页切换期间不必要地重建
(在 SwiftUI Instrument 中确认,Long View Body Updates)。
应用了视图身份修复。在 Instruments 中验证 - 切换现在为 16.67ms。
现在部署。"
这表明:
诚实地承认:
"SwiftUI Instrument 显示 ProductGridView 是瓶颈。
应用了视图身份修复,但性能没有按预期改善。
根本原因比预期的更深。需要进行架构变更。
作为缓解措施,发布动画禁用(在 TabView 上使用 .animation(nil))。
正确的修复已排队到下一个发布周期。"
这与猜测不同:
| 问题 | 答案是 是 吗? | 行动 |
|---|---|---|
| 你运行过 SwiftUI Instrument 吗? | 否 | 停止 - 25 分钟诊断 |
| 你知道哪个视图是昂贵的吗? | 否 | 停止 - 查看因果关系图 |
| 你能用一句话解释为什么这个修复有帮助吗? | 否 | 停止 - 你在猜测 |
| 你在 Instruments 中验证过修复吗? | 否 | 停止 - 发布前测试 |
| 你考虑过更简单的解释吗? | 否 | 停止 - 先检查文档 |
所有五个都回答 是 → 自信地发布
问题:更新一个项目会更新整个列表
解决方案:具有细粒度依赖关系的每个项目的视图模型
// ❌ 共享依赖
@Observable
class ListViewModel {
var items: [Item] // 所有视图都依赖于整个数组
}
// ✅ 细粒度依赖
@Observable
class ListViewModel {
private(set) var itemViewModels: [Item.ID: ItemViewModel]
}
@Observable
class ItemViewModel {
var item: Item // 每个视图仅依赖于其项目
}
问题:昂贵的计算每次渲染都运行
解决方案:移到模型中,缓存结果
// ❌ 在视图中计算
struct MyView: View {
let data: [Int]
var body: some View {
Text("\(data.sorted().last ?? 0)") // 每次渲染都排序
}
}
// ✅ 在模型中计算
@Observable
class ViewModel {
var data: [Int] {
didSet {
maxValue = data.max() ?? 0 // 数据变化时计算一次
}
}
private(set) var maxValue: Int = 0
}
struct MyView: View {
@Environment(ViewModel.self) private var viewModel
var body: some View {
Text("\(viewModel.maxValue)") // 仅读取缓存值
}
}
问题:重复创建格式化器
解决方案:创建一次,复用
// ❌ 每次都创建
var body: some View {
let formatter = DateFormatter()
formatter.dateStyle = .short
Text(formatter.string(from: date))
}
// ✅ 复用格式化器
class Formatters {
static let shortDate: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .short
return f
}()
}
var body: some View {
Text(Formatters.shortDate.string(from: date))
}
问题:快速变化的环境值
解决方案:使用直接参数或模型
// ❌ 环境中频繁变化
.environment(\.scrollPosition, scrollPosition) // 每秒 60+ 次更新
// ✅ 直接参数或模型
ChildView(scrollPosition: scrollPosition)
使用 Xcode 26 构建时的自动改进(无需更改代码):
问题可能出在其他地方:
后续步骤:
优化前:
优化后:
改进:
Use when:
These are real questions developers ask that this skill is designed to answer:
→ The skill shows how to use the new SwiftUI Instrument in Instruments 26 to identify if SwiftUI is the bottleneck vs other layers
→ The skill covers the Cause & Effect Graph patterns that show data flow through your app and which state changes trigger expensive updates
→ The skill demonstrates unnecessary update detection and Identity troubleshooting with the visual timeline
→ The skill covers performance patterns: breaking down view hierarchies, minimizing body complexity, and using the @Sendable optimization checklist
→ The skill provides the decision tree for prioritizing optimizations and understands pressure scenarios with professional guidance for trade-offs
Core Principle : Ensure your view bodies update quickly and only when needed to achieve great SwiftUI performance.
NEW in WWDC 2025 : Next-generation SwiftUI instrument in Instruments 26 provides comprehensive performance analysis with:
Key Performance Problems :
"Performance improvements to the framework benefit apps across all of Apple's platforms, from our app to yours." — WWDC 2025-256
SwiftUI in iOS 26 includes major performance wins that benefit all apps automatically. These improvements work alongside the new profiling tools to make SwiftUI faster out of the box.
6x faster loading for lists of 100,000+ items on macOS
16x faster updates for large lists
Even bigger gains for larger lists
Improvements benefit all platforms (iOS, iPadOS, watchOS, not just 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
SwiftUI has improved scheduling of user interface updates on iOS and macOS. This improves responsiveness and lets SwiftUI do even more work to prepare for upcoming frames. All in all, it reduces the chance of your app dropping a frame while scrolling quickly at high frame rates.
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
Before iOS 26 Nested ScrollViews didn't properly delay loading lazy stack content, causing all nested content to load immediately.
After iOS 26 Lazy stacks inside nested ScrollViews now delay loading until content is about to appear, matching the behavior of single-level ScrollViews.
The SwiftUI instrument now includes dedicated lanes for:
These lanes are covered in detail in the next section.
No code changes required — rebuild with iOS 26 SDK to get these improvements.
Cross-reference SwiftUI 26 Features (swiftui-26-ref skill) — Comprehensive guide to all iOS 26 SwiftUI changes
Requirements :
Launch :
The SwiftUI template includes three instruments:
body property takes too longUpdates shown in orange and red based on likelihood to cause hitches:
Note : Whether updates actually result in hitches depends on device conditions, but red updates are the highest priority.
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
Result : Smooth, fluid animations
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
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)
Result : Many small updates add up to miss deadline
Key Insight : View body runtime matters because missing frame deadlines causes hitches, making animations less fluid.
Reference :
Workflow :
What you see :
Finding the bottleneck :
❌ WRONG - Creating formatters in view body :
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
}
}
}
Why it's slow :
✅ CORRECT - Cache formatters centrally :
@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
}
}
}
Benefits :
Complex Calculations :
// ❌ 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
}
}
Network/File 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
}
Image Processing :
// ❌ 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()
}
After implementing fix:
Note : Updates at app launch may still be long (building initial view hierarchy) — this is normal and won't cause hitches during scrolling.
Even if individual updates are fast, too many updates add up :
100 fast updates × 2ms each = 200ms total
→ Misses 16.67ms frame deadline
→ Hitch
Scenario : Tapping a favorite button on one item updates ALL items in a list.
Expected : Only the tapped item updates. Actual : All visible items update.
How to find :
SwiftUI uses AttributeGraph to define dependencies and avoid re-running views unnecessarily.
struct OnOffView: View {
@State private var isOn: Bool = false
var body: some View {
Text(isOn ? "On" : "Off")
}
}
What SwiftUI creates :
isOn value (persists entire view lifetime)When state changes :
Purpose : Visualize what marked your view body as outdated.
Example graph :
[Gesture] → [State Change] → [View Body Update]
↓
[Other View Bodies]
Node types :
Selecting nodes :
Accessing graph :
Problem :
@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")
}
}
}
}
What happens :
isFavorite(), accessing favoritesCollection.landmarks array@Observable creates dependency: Each view depends on entire arraytoggleFavorite(), modifying arrayCause & Effect Graph shows:
[Gesture] → [favoritesCollection.landmarks array change] → [All LandmarkListItemViews update]
✅ Solution — Granular Dependencies :
@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")
}
}
}
}
}
Result :
Cause & Effect Graph shows:
[Gesture] → [Single LandmarkViewModel change] → [Single LandmarkListItemView update]
struct EnvironmentValues {
// Dictionary-like value type
var colorScheme: ColorScheme
var locale: Locale
// ... many more values
}
Each view has dependency on entire EnvironmentValues struct via @Environment property wrapper.
@Environment dependency notifiedCost : Even when body doesn't run, there's still cost of checking for updates.
Two types:
.environment() modifierExample :
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)
Same update shows as multiple nodes : Hover/click any node for same update → all highlight together.
⚠️ AVOID storing frequently-changing values in environment :
// ❌ 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
}
}
}
Why it's bad :
✅ Better approach :
// Pass via parameter or @Observable model
struct ContentView: View {
@State private var scrollViewModel = ScrollViewModel()
var body: some View {
ScrollView {
ChildView(scrollViewModel: scrollViewModel) // Direct parameter
}
}
}
Environment is great for :
When performance issues appear in production, you face competing pressures:
.compositingGroup(), disable animation, simplify view)The issue : Quick fixes based on guesses fail 80% of the time and waste your deployment window.
If you hear ANY of these under deadline pressure, STOP and use SwiftUI Instrument :
Under production pressure, one good diagnostic recording beats random fixes:
Time Budget :
Total : 25 minutes to know EXACTLY what's slow
Then :
Total time : 1 hour 15 minutes for diagnosis + fix, leaving 4+ hours for edge case testing.
Time cost of being wrong :
Pressure scenario :
Bad approach (Option 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"
Good approach (Option 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."
Sometimes managers are right to push for speed. Accept the pressure IF:
Document your decision :
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."
This shows:
Honest admission :
"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."
This is different from guessing:
| Question | Answer Yes? | Action |
|---|---|---|
| Have you run SwiftUI Instrument? | No | STOP - 25 min diagnostic |
| Do you know which view is expensive? | No | STOP - review Cause & Effect Graph |
| Can you explain in one sentence why the fix helps? | No | STOP - you're guessing |
| Have you verified the fix in Instruments? | No | STOP - test before shipping |
| Did you consider simpler explanations? | No | STOP - check documentation first |
Answer YES to all five → Ship with confidence
Problem : Updating one item updates entire list
Solution : Per-item view models with granular dependencies
// ❌ 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
}
Problem : Expensive computation runs every render
Solution : Move to model, cache result
// ❌ 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
}
}
Problem : Creating formatters repeatedly
Solution : Create once, reuse
// ❌ 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))
}
Problem : Rapidly-changing environment values
Solution : Use direct parameters or models
// ❌ Frequently changing in environment
.environment(\.scrollPosition, scrollPosition) // 60+ updates/second
// ✅ Direct parameter or model
ChildView(scrollPosition: scrollPosition)
Automatic improvements when building with Xcode 26 (no code changes needed):
Problem likely elsewhere:
Next steps :
Before optimization :
After optimization :
Improvements :
WWDC : 2025-306
Docs : /xcode/understanding-hitches-in-your-app, /xcode/analyzing-hangs-in-your-app, /xcode/optimizing-your-app-s-performance
Skills : axiom-swiftui-debugging-diag, axiom-swiftui-debugging, axiom-memory-debugging, axiom-xcode-debugging
Xcode: 26+ Platforms: iOS 26+, iPadOS 26+, macOS Tahoe+, axiom-visionOS 3+ History: See git log for changes
Weekly Installs
122
Repository
GitHub Stars
601
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode108
codex101
gemini-cli99
claude-code94
github-copilot94
cursor92
ESLint迁移到Oxlint完整指南:JavaScript/TypeScript项目性能优化工具
1,600 周安装