使用程式碼審查、Instruments 工具與可重複測量來分析、診斷並修復 SwiftUI 執行時期效能。當 SwiftUI 畫面渲染緩慢、捲動或動畫卡頓、View body 過度更新、列表識別碼頻繁變動、佈局工作暴增,或廣泛的 Observation 依賴導致 CPU 成本上升時使用。涵蓋基於證據的初步判斷、SwiftUI Instruments 軌道、懶加載容器的使用規範、狀態生命週期,以及改善前後的驗證。
SwiftUI 效能
從可重現的症狀到可測量的改善,審查 SwiftUI 檢視效能。動畫設計請參考 swiftui-animation,生產環境遙測請參考 metrickit,所有權/記憶體洩漏分析請參考 ios-memgraph-analysis,導航行為請參考 swiftui-navigation,狀態架構請參考 swiftui-patterns,佈局建構請參考 swiftui-layout-components。
目錄
- 工作流程決策樹
- 1. 程式碼優先審查
- 2. 引導使用者進行效能剖析
- 3. 分析與診斷
- 4. 修復
- 常見程式碼異味(與修復方式)
- 5. 驗證
- 輸出
- Instruments 效能剖析
- 識別碼與生命週期
- 懶加載模式
- 狀態與 Observation 最佳化
- 常見錯誤
- 審查檢查清單
- 參考資料
工作流程決策樹
- 有提供程式碼:先審查程式碼,並將發現標記為假設。
- 僅有症狀:收集最小的相關檢視、資料流、重現步驟、裝置、作業系統與建置設定。
- 審查無結論:在建議大規模重構前,先收集追蹤記錄或軌道截圖。
使用以下初步判斷清單進行程式碼與追蹤分析:
- 廣泛的狀態依賴或失效風暴
- 不穩定的列表識別碼或根條件切換
- 在
body中進行格式化、排序、解碼或同步 I/O - 佈局/幾何回饋迴圈與過大的圖片
- 對大型檢視層級套用隱式動畫
1. 程式碼優先審查
將初步判斷清單中的每個可疑項目對應到確切的程式碼。報告可能的原因並附上程式碼參考,但在追蹤記錄確認成本之前,將其標記為基於程式碼的假設。當證據不足時,建議建立最小重現範例或進行測量。
2. 引導使用者進行效能剖析
使用 Release 建置 與真實裝置進行 SwiftUI Instruments 範本剖析。重現確切的互動,並擷取相關的 SwiftUI 軌道、Time Profiler 與 Hangs/Hitches。要求提供追蹤記錄或軌道與呼叫樹的截圖。
3. 分析與診斷
將相同的初步判斷清單應用於追蹤證據。將長時間或頻繁的 SwiftUI 更新與 Time Profiler 呼叫樹及重現的互動進行關聯。區分基於追蹤的發現與基於程式碼的假設,並指出能解決剩餘不確定性的下一步測量。
4. 修復
套用針對性的修復:
- 縮小狀態範圍(將
@State/@Observable移至更接近葉節點檢視)。 - 穩定
ForEach與列表的識別碼。 - 將耗時工作移出
body,改為模型層預先計算、在輸入變更時更新的明確衍生值、記憶化輔助函式或背景處理。
僅在檢視同時擁有值與其更新生命週期時使用@State;它不是任意計算的通用快取。 - 僅在相等性檢查比重新計算子樹更便宜,且比較的輸入具有穩定的值語義時使用
equatable()。 - 在渲染前縮小圖片尺寸。
- 減少佈局複雜度,或盡可能使用固定尺寸。
常見程式碼異味(與修復方式)
| 異味 | 需尋找的證據 | 針對性修復 |
|---|---|---|
在 body 中進行格式化、排序、過濾或解碼 |
長時間/頻繁的 body 更新,且呼叫樹成本相符 | 在輸入變更時重新計算;在主執行緒外進行縮小/解碼 |
UUID() 或不穩定的 id: \.self |
行被重新建立、狀態遺失、過多更新 | 使用穩定的模型識別碼 |
根 if/else 切換 |
切換時狀態重置或更新暴增 | 在語義允許的情況下,將條件內容/修飾符局部化 |
| 廣泛的模型讀取 | 許多不相關的檢視同時更新 | 傳遞狹義的值,或將讀取移至專注的子檢視 |
| 在佈局期間寫入幾何屬性 | 重複的佈局/更新循環 | 設定變化閾值,或用穩定的佈局取代回饋路徑 |
5. 驗證
要求使用者重新執行相同的擷取,並與基準指標進行比較。如果有提供,總結差異(CPU、掉幀、記憶體峰值)。
輸出
提供:
- 簡短的指標表格(如有前後對比)。
- 主要問題(按影響程度排序)。
- 建議的修復方式與預估工作量。
Instruments 效能剖析
在 Instruments 中使用 SwiftUI 範本(Cmd+I 進行剖析)。目前的 SwiftUI 軌道包括 Update Groups、Long View Body Updates、Long Representable Updates / Representable Updates、Other Long Updates / Other Updates,以及 Cause & Effect Graph。將這些與 Time Profiler 和 Hangs/Hitches 進行關聯。
在除錯建置中加入 Self._printChanges() 來記錄觸發檢視更新的屬性:
var body: some View {
#if DEBUG
let _ = Self._printChanges() // "MyView: @self, _count changed."
#endif
Text("Count: \(count)")
}
完整的剖析工作流程請參閱 references/optimizing-swiftui-performance-instruments.md。
識別碼與生命週期
識別碼控制檢視的生命週期與狀態。在重複內容中使用穩定的模型 ID,並保留 .id(_:) 變更用於有意的重置。在剖析的熱點行中,優先使用 @ViewBuilder 或泛型組合而非 AnyView。將根條件分支視為可疑對象——但非自動缺陷——當證據顯示狀態頻繁變動或昂貴的重新建立時。
Text(title)
.foregroundStyle(isHighlighted ? .yellow : .primary)
ForEach(items) { item in
Row(item: item).id(item.stableID)
}
懶加載模式
當剖析顯示積極建構、佈局或更新工作確實存在時,使用懶加載容器;沒有通用的項目數量閾值。網格/列表建構選擇請參考 swiftui-layout-components。
使用規範:
- 離開畫面的檢視會從懶加載堆疊中移除。SwiftUI 可能會短暫保留它們,然後刪除檢視及其檢視區域狀態。
- 如果重要的行狀態必須在捲動離開後保留,請將其保存在行檢視之外。
- 由於預先載入,body 與佈局工作可能在
onAppear之前發生。不要將onAppear設為行渲染所需資料的唯一設定點。 - 將
onAppear與onDisappear視為可見性訊號,而非生命週期保證。 - 在
ForEach之前過濾資料;避免使用if分支使每個元素產生零或一行。 - 保持每個
ForEach元素具有固定數量的頂層子檢視。如有需要,將行內容包裝在穩定的容器中。在除錯列表/表格慢路徑時使用-LogForEachSlowPath YES。 - 避免絕對的內容尺寸或內容偏移假設;懶加載堆疊會估算畫面外尺寸。
- 避免在懶加載行中出現幾何回饋迴圈。在將幾何變更回饋到行狀態之前,優先使用穩定尺寸、佈局基本元件或自訂
Layout。
狀態與 Observation 最佳化
Observation 會追蹤在檢視評估期間讀取的屬性。透過傳遞狹義的衍生值或將讀取移至專注的子檢視來減少扇出。
// 將讀取拆分到子檢視,使每個檢視只追蹤它渲染的內容。
struct ProfileView: View {
let model: ProfileModel
var body: some View {
VStack {
NameRow(model: model) // 只追蹤名稱
EmailRow(model: model) // 只追蹤電子郵件
AvatarView(model: model) // 只追蹤頭像
SettingsForm(model: model) // 只追蹤設定
}
}
}
便宜的計算值可以保持為衍生值;昂貴的轉換需要明確的擁有者、輸入集合與重新整理觸發器。不要將檢視模型作為效能儀式加入——先測量,一般狀態設計請參考 swiftui-patterns。
常見錯誤
- 對 Debug 建置進行剖析。 Debug 建置包含額外的執行時期檢查並停用最佳化,會產生誤導性的效能資料。請在真實裝置上對 Release 建置進行剖析。
- 當只需要一個屬性時,觀察整個模型。 將大型
@Observable模型拆分為專注的模型,或使用計算屬性/閉包來縮小觀察範圍。 - 在 ScrollView 項目中使用幾何回饋。 GeometryReader 或雜訊較多的幾何狀態可能導致重複佈局。優先使用穩定尺寸、自訂佈局,或設定閾值的狹義
.onGeometryChange(iOS 16+)。 - 在
body中呼叫DateFormatter()或NumberFormatter()。 這些物件的建立成本很高。請將其設為靜態或移至檢視外部。 - 對非 Equatable 狀態進行動畫。 如果 SwiftUI 無法判斷相等性,它會每幀重新繪製。將狀態符合
Equatable,然後對簡單的值綁定變更使用.animation(_:value:),或對更狹義的修飾符範圍隱式動畫使用.animation(_:body:)。 - 大型扁平
List沒有識別碼。 使用id:或將項目設為Identifiable,以便 SwiftUI 能有效進行差異化,而不是重建整個列表。 - 不必要的
@State包裝物件。 將簡單的值型別包裝在類別中以供@State使用,會破壞值語義。請對結構體使用純@State。 - 使用同步 I/O 阻塞
MainActor。 檔案讀取、大型 JSON 解析與圖片解碼應在主執行緒之外進行。優先使用非隔離的非同步輔助函式或專用的 actor;僅在有意打破 actor 繼承並自行處理取消時,才保留Task.detached。
審查檢查清單
- [ ]
body內部沒有DateFormatter/NumberFormatter的配置 - [ ] 大型列表使用
Identifiable項目或明確的id: - [ ]
@Observable模型僅暴露檢視實際讀取的屬性 - [ ] 耗時計算(圖片處理、解析)在
MainActor之外進行 - [ ] 懶加載行具有穩定的識別碼、固定的頂層行形狀與預先過濾的資料
- [ ] 捲動行中的幾何變更已設定閾值,且不回饋到廣泛的狀態
- [ ] 行渲染不依賴
onAppear作為唯一的設定點 - [ ] 隱式動畫對值綁定變更使用
.animation(_:value:),或對更狹義的修飾符範圍使用.animation(_:body:) - [ ] 主執行緒上沒有同步的網路/檔案 I/O
- [ ] 在 Release 建置、真實裝置上進行剖析
- [ ]
@State未用作未指定的快取;每個衍生值都有明確的擁有者與重新整理觸發器 - [ ] 僅在比較比重新計算更便宜且輸入具有穩定的值語義時使用
equatable() - [ ] 發現結果區分基於程式碼的假設與基於追蹤的證據
- [ ]
@Observable檢視模型已隔離至@MainActor;跨越並發邊界的型別為Sendable
參考資料
- 揭開 SwiftUI 效能的奧秘(WWDC23):references/demystify-swiftui-performance-wwdc23.md
- 使用 Instruments 最佳化 SwiftUI 效能:references/optimizing-swiftui-performance-instruments.md
- 了解應用程式中的卡頓:references/understanding-hangs-in-your-app.md
- 了解與改善 SwiftUI 效能:references/understanding-improving-swiftui-performance.md
- WWDC 逐字稿來源:references/wwdc-session-sources.md






