swiftui-performance

swiftui-performance

熱門

使用程式碼審查、Instruments 工具與可重複測量來分析、診斷並修復 SwiftUI 執行時期效能。當 SwiftUI 畫面渲染緩慢、捲動或動畫卡頓、View body 過度更新、列表識別碼頻繁變動、佈局工作暴增,或廣泛的 Observation 依賴導致 CPU 成本上升時使用。涵蓋基於證據的初步判斷、SwiftUI Instruments 軌道、懶加載容器的使用規範、狀態生命週期,以及改善前後的驗證。

927星標
46分支
更新於 2026/7/15
SKILL.md
readonlyread-only
name
swiftui-performance
description

使用程式碼審查、Instruments 工具與可重複測量來分析、診斷並修復 SwiftUI 執行時期效能。當 SwiftUI 畫面渲染緩慢、捲動或動畫卡頓、View body 過度更新、列表識別碼頻繁變動、佈局工作暴增,或廣泛的 Observation 依賴導致 CPU 成本上升時使用。涵蓋基於證據的初步判斷、SwiftUI Instruments 軌道、懶加載容器的使用規範、狀態生命週期,以及改善前後的驗證。

SwiftUI 效能

從可重現的症狀到可測量的改善,審查 SwiftUI 檢視效能。動畫設計請參考 swiftui-animation,生產環境遙測請參考 metrickit,所有權/記憶體洩漏分析請參考 ios-memgraph-analysis,導航行為請參考 swiftui-navigation,狀態架構請參考 swiftui-patterns,佈局建構請參考 swiftui-layout-components

目錄

工作流程決策樹

  • 有提供程式碼:先審查程式碼,並將發現標記為假設。
  • 僅有症狀:收集最小的相關檢視、資料流、重現步驟、裝置、作業系統與建置設定。
  • 審查無結論:在建議大規模重構前,先收集追蹤記錄或軌道截圖。

使用以下初步判斷清單進行程式碼與追蹤分析:

  • 廣泛的狀態依賴或失效風暴
  • 不穩定的列表識別碼或根條件切換
  • 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 設為行渲染所需資料的唯一設定點。
  • onAppearonDisappear 視為可見性訊號,而非生命週期保證。
  • 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

常見錯誤

  1. 對 Debug 建置進行剖析。 Debug 建置包含額外的執行時期檢查並停用最佳化,會產生誤導性的效能資料。請在真實裝置上對 Release 建置進行剖析。
  2. 當只需要一個屬性時,觀察整個模型。 將大型 @Observable 模型拆分為專注的模型,或使用計算屬性/閉包來縮小觀察範圍。
  3. 在 ScrollView 項目中使用幾何回饋。 GeometryReader 或雜訊較多的幾何狀態可能導致重複佈局。優先使用穩定尺寸、自訂佈局,或設定閾值的狹義 .onGeometryChange(iOS 16+)。
  4. body 中呼叫 DateFormatter()NumberFormatter() 這些物件的建立成本很高。請將其設為靜態或移至檢視外部。
  5. 對非 Equatable 狀態進行動畫。 如果 SwiftUI 無法判斷相等性,它會每幀重新繪製。將狀態符合 Equatable,然後對簡單的值綁定變更使用 .animation(_:value:),或對更狹義的修飾符範圍隱式動畫使用 .animation(_:body:)
  6. 大型扁平 List 沒有識別碼。 使用 id: 或將項目設為 Identifiable,以便 SwiftUI 能有效進行差異化,而不是重建整個列表。
  7. 不必要的 @State 包裝物件。 將簡單的值型別包裝在類別中以供 @State 使用,會破壞值語義。請對結構體使用純 @State
  8. 使用同步 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

參考資料