mcpField
A
axiom-swiftui-performance

axiom-swiftui-performance

github.com/charleswiltgen開発1.2k215 回インストール今日更新
リポジトリ

axiom-swiftui-performance について

SwiftUI 開発者向けのパフォーマンス分析・最適化スキルです。Instruments 26 を使い、遅いビュー更新や不要な再更新を特定する方法と最適化の考え方を解説します。

主な機能

  1. 01SwiftUI Instrument による分析
  2. 02更新の因果関係グラフの確認
  3. 03時間のかかる body 更新の特定
  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 のパフォーマンス最適化

このスキルを使う場面

次のような場合に使用します:

  • アプリの応答性が低下している(引っかかり、フリーズ、スクロールの遅延)
  • アニメーションが一時停止したり、実行中にカクついたりする
  • スクロールのパフォーマンスが低い
  • プロファイリングで SwiftUI がボトルネックだと判明した
  • ビューのボディの実行に時間がかかりすぎている
  • ビューが必要以上に頻繁に更新されている
  • SwiftUI の更新による因果関係を理解する必要がある

プロンプトの例

このスキルで回答できる、開発者から実際によく寄せられる質問を紹介します:

1. 「アプリのスクロールがカクつき、アニメーションも途切れます。SwiftUI が原因かどうかを調べるにはどうすればよいですか?」

→ このスキルでは、Instruments 26 の新しい SwiftUI Instrument を使って、SwiftUI とほかのレイヤーのどちらがボトルネックかを特定する方法を紹介します

2. 「新しい SwiftUI Instrument を使っていますが、長い更新を示すオレンジ色や赤色のバーが表示されます。原因を特定するにはどうすればよいですか?」

→ このスキルでは、アプリ内のデータフローや、コストの高い更新を引き起こす状態変更を示す Cause & Effect Graph のパターンを解説します

3. 「データが変わっていないのに、更新されすぎているビューがあります。問題のビューを見つけるにはどうすればよいですか?」

→ このスキルでは、ビジュアルタイムラインを使った不要な更新の検出と Identity のトラブルシューティングを実演します

4. 「大規模なデータ構造と複雑なビュー階層を扱っています。SwiftUI のパフォーマンス向上のために最適化するにはどうすればよいですか?」

→ このスキルでは、ビュー階層の分割、ボディの複雑さの最小化、@Sendable 最適化チェックリストの活用など、パフォーマンスに関するパターンを解説します

5. 「パフォーマンス改善の期限が迫っていて、SwiftUI のどこに時間がかかっているかを把握する必要があります。重要な指標は何ですか?」

→ このスキルでは、最適化の優先順位を決めるための判断ツリーを示し、トレードオフに関する専門的なガイダンスとともに、時間的なプレッシャーがある状況への対処方法を解説します


概要

基本原則:優れた SwiftUI パフォーマンスを実現するには、ビューのボディがすばやく、必要なときだけ更新されるようにします。

WWDC 2025 の新機能:Instruments 26 の次世代 SwiftUI instrument では、包括的なパフォーマンス分析が可能です:

  • 長時間の更新を示すビジュアルタイムライン(深刻度に応じてオレンジ色/赤色で色分け)
  • アプリ内のデータフローを示す Cause & Effect Graph
  • CPU 分析のための Time Profiler との連携
  • ハングとヒッチの追跡

主なパフォーマンス問題:

  1. ビューのボディの長時間更新 — ビューのボディの実行に時間がかかりすぎる
  2. 不要なビューの更新 — データが実際には変わっていないのにビューが更新される

iOS 26 のフレームワークのパフォーマンス改善

「フレームワークのパフォーマンス改善は、私たちのアプリから皆さんのアプリまで、Apple のすべてのプラットフォーム上のアプリにメリットをもたらします。」 — WWDC 2025-256

iOS 26 の SwiftUI には、すべてのアプリに自動的にメリットをもたらす大幅なパフォーマンス向上が含まれています。これらの改善は新しいプロファイリングツールと連携し、SwiftUI をすぐに使える状態で高速化します。

リストのパフォーマンス(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
アプリへの影響
  • 大規模なデータセット(10k 件以上)で、目に見える改善が得られます
  • フィルタリングと並べ替えの処理がより速く完了します
  • リストへのリアルタイム更新に対する応答性が向上します
  • ファイルブラウザー、連絡先リスト、データテーブルなどのアプリにメリットがあります
スクロールのパフォーマンス
高速スクロール中のフレーム落ちを軽減

SwiftUI では、iOS と macOS におけるユーザーインターフェイス更新のスケジューリングが改善されました。これにより応答性が向上し、SwiftUI は次のフレームに備えてさらに多くの処理を行えるようになります。全体として、高いフレームレートで高速スクロールするときに、アプリでフレーム落ちが発生する可能性を低減します。

主な改善点
  1. フレームスケジューリングの改善 — SwiftUI が次のフレームに備える時間が増えます
  2. 応答性の向上 — UI の更新がより効率的にスケジュールされます
  3. フレーム落ちの減少 — 特に 120Hz(ProMotion)で素早くスクロールする場合に効果があります
効果を実感できる場面
  • 画像の多いコンテンツをスクロールするとき
  • 高フレームレートのデバイス(iPhone Pro、ProMotion 搭載の iPad Pro)
  • 複数のビューを含む複雑なリスト行
Lazy Stack を含むネストされた ScrollView
写真カルーセルや複数軸のスクロールが適切に最適化されます
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 より前 ネストされた ScrollView では、Lazy Stack のコンテンツの読み込みが適切に遅延されず、ネストされたすべてのコンテンツがすぐに読み込まれていました。

iOS 26 以降 ネストされた ScrollView 内の Lazy Stack では、コンテンツが表示される直前まで読み込みが遅延され、単一階層の ScrollView と同じ動作になります。

ユースケース
  • 横方向/縦方向にスクロールできる写真ギャラリー
  • Netflix 形式のカテゴリ行
  • 多次元データブラウザー
  • 縦方向の詳細スクロールを備えた画像カルーセル
SwiftUI パフォーマンスインストゥルメントの強化
Instruments 26 の新しいレーン

SwiftUI インストゥルメントに、次の専用レーンが追加されました。

  1. 長時間の View Body 更新 — コストの高い body の計算を特定します
  2. プラットフォーム View の更新 — UIKit/AppKit とのブリッジ処理のパフォーマンスを追跡します(長時間の Representable 更新)
  3. その他の長時間の更新 — その他すべての種類の、時間のかかる SwiftUI 処理

これらのレーンについては、次のセクションで詳しく説明します。

パフォーマンス改善の概要
自動的に適用される改善(再コンパイルのみ)
  • ✅ リストの読み込みが 6 倍高速化(10 万件以上、macOS)
  • ✅ リストの更新が 16 倍高速化(macOS)
  • ✅ スクロール中のフレーム落ちを軽減
  • ✅ iOS/macOS でのフレームスケジューリングを改善
  • ✅ ネストされた ScrollView の遅延読み込みを最適化

コードの変更は不要です — iOS 26 SDK で再ビルドすると、これらの改善が適用されます。

関連項目 SwiftUI 26 の機能(swiftui-26-ref スキル)— iOS 26 の SwiftUI の変更点を網羅したガイド


SwiftUI インストゥルメント(Instruments 26)

はじめに

要件:

  • Xcode 26 をインストールします
  • デバイスを最新の OS リリースにアップデートします(SwiftUI トレースの記録に対応)
  • 正確なプロファイリングのため、アプリをリリースモードでビルドします

起動方法:

  1. Xcode でプロジェクトを開きます
  2. Command-I を押してプロファイルを実行します
  3. テンプレート選択画面で SwiftUI テンプレート を選択します
  4. 記録ボタンをクリックします
テンプレートの内容

SwiftUI テンプレートには、次の 3 つのインストゥルメントが含まれます。

  1. SwiftUI インストゥルメント(新機能)— SwiftUI コードのパフォーマンス問題を特定します
  2. Time Profiler — 時間経過に伴う CPU 処理のサンプルを表示します
  3. ハングとヒッチ — アプリの応答性を追跡します
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)

結果:小さな更新が積み重なり、締め切りに間に合わなくなります。

重要なポイント:フレームの締め切りに間に合わないと hitch が発生し、アニメーションが滑らかでなくなるため、View のボディの実行時間が重要です。

参考資料:


問題 1:View のボディの更新に時間がかかる

時間のかかる更新を特定する
  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 のインストゥルメントトラックに切り替えます

表示される内容:

  • View のボディの実行中に記録されたサンプルのコールスタック
  • 各フレームに費やされた時間(左端の列)
  • 深い SwiftUI コールスタック内にネストされた View のボディ

ボトルネックを見つける方法:

  1. Option キーを押しながらクリックしてメインスレッドのコールスタックを展開します
  2. Command-F で View の名前(例:「LandmarkListItemView」)を検索します
  3. 時間の列で負荷の高い処理を特定します
よくある負荷の高い処理
Formatter の作成(非常に負荷が高い)

❌ 誤り — View のボディ内で formatter を作成する:

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
        }
    }
}

時間がかかる理由:

  • Formatter の作成には時間がかかります(1つあたり数ミリ秒)
  • View のボディが実行されるたびに作成されます
  • メインスレッドで実行されるため、UI の更新を続行する前にアプリが待機します
  • 複数の View では、時間がすぐに積み重なります

✅ 正解 — Formatter を一元的にキャッシュする:

@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
        }
    }
}

利点:

  • Formatter は一度だけ作成され、すべてのランドマークで再利用されます
  • 位置情報が変わったときに文字列が事前に計算されます
  • View のボディはキャッシュされた値を読み取るだけなので、瞬時に完了します
  • View のボディに時間がかかる更新がなくなります
その他の負荷の高い処理

複雑な計算:

// ❌ 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. 一覧から対象の View が消えていること(または大幅に減少していること)を確認します

注:アプリ起動時の更新には、引き続き時間がかかる場合があります(初期の View 階層を構築するため)。これは正常で、スクロール中に hitch が発生することはありません。


問題 2:不要な View の更新

不要な更新が問題となる理由

個々の更新が速くても、更新が多すぎると積み重なります:

100 fast updates × 2ms each = 200ms total
→ Misses 16.67ms frame deadline
→ Hitch
不要な更新を特定する

状況:項目の1つにあるお気に入りボタンをタップすると、リスト内のすべての項目が更新されます。

期待される動作:タップした項目だけが更新されます。 実際の動作:表示されているすべての項目が更新されます。

特定方法:

  1. ユーザー操作を意識してトレースを記録します
  2. タイムラインの該当箇所をハイライトします
  3. 詳細ペインで階層を展開します
  4. 更新回数を数えます — 想定より多くありませんか?
SwiftUI のデータモデルを理解する

SwiftUI はAttributeGraphを使用して依存関係を定義し、View の不要な再実行を防ぎます。

属性と依存関係
struct OnOffView: View {
    @State private var isOn: Bool = false

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

SwiftUI が作成するもの:

  1. View 属性 — View の構造体を格納します(頻繁に再作成されます)
  2. State ストレージ — isOn の値を保持します(View のライフタイム全体にわたって維持されます)
  3. シグナル属性 — State が変更されたタイミングを追跡します
  4. View の body 属性 — State シグナルに依存します
  5. Text 属性 — View の body に依存します

State が変更されたとき:

  1. トランザクションを作成します(次のフレームに予定された変更)
  2. シグナル属性を古い状態としてマークします
  3. 依存関係チェーンをたどり、依存する属性を古い状態としてマークします(フラグを設定するだけなので高速です)
  4. レンダリングの前に、古い状態になったすべての属性を更新します
  5. View のボディが再実行され、新しい 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. すべてのビューのボディが実行されます(変更されたのは1つだけでも)

原因と結果のグラフの表示:

[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")
                }
            }
        }
    }
}

結果:

  • 各ビューは自身のビューモデルのみに依存します
  • ボタンをタップすると、そのビューモデルのみが更新されます
  • 1つのビューのボディだけが実行されます

原因と結果のグラフの表示:

[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 はビューのボディの実行をスキップします(すでに最新の状態です)

コスト:ボディが実行されない場合でも、更新を確認するコストは発生します。

グラフ内の環境更新ノード

2種類あります。

  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
        }
    }
}

問題となる理由:

  • 環境の変更により、すべての子ビューで確認処理が発生します
  • スクロールすると、1秒あたり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 モードにはオーバーヘッドがあります)
  • 実機でテストする(シミュレーターのパフォーマンスは実機と異なります)
  • デバイスの OS を最新にする(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 の記録 1 回で行う診断(30 分プロトコル)

本番環境でプレッシャーがかかっているときは、無作為な修正より、適切な診断記録を 1 回取るほうが効果的です。

時間の配分:

  • 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 で 1 回記録する
  • 診断時間:25 分
  • 対象を絞った修正の適用時間:20 分
  • 検証時間:5 分
  • デプロイ時間:20 分
  • 合計時間:1.5 時間
  • ユーザーへの影響:26 時間以上ではなく、2 時間後に解消

判断を誤った場合の時間的なコスト:

  • 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 の記録を 1 回実行している(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 を実行しましたか?いいえSTOP - 25分間の診断
どのビューが高負荷か把握していますか?いいえSTOP - Cause & Effect Graph を確認
修正が役立つ理由を一文で説明できますか?いいえSTOP - 推測で進めています
Instruments で修正を検証しましたか?いいえSTOP - リリース前にテストしてください
より単純な説明を検討しましたか?いいえSTOP - まずドキュメントを確認してください

5つすべてに「はい」と回答 → 自信を持ってリリースできます


よくあるパターンと解決策

パターン 1:リスト項目の依存関係

問題: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:安定した値にのみ Environment を使用する

問題:Environment の値が頻繁に変化する

解決策:直接パラメーターまたはモデルを使用します

// ❌ 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. 問題の種類を特定する:
    • View Body Updates が長い? → Long Updates のセクションへ
    • 更新が多すぎる? → Unnecessary Updates のセクションへ
  5. 長時間の更新には Time Profiler を使用します(高負荷な処理を見つけます)
  6. 不要な更新には Cause & Effect Graph を使用します(依存関係の問題を見つけます)
  7. 修正を実装する
  8. 新しいトレースで検証する
SwiftUI が原因ではない場合
パフォーマンス問題が発生している間、Update Groups レーンが空ですか?

問題は別の場所にある可能性があります:

  • ネットワークリクエスト
  • バックグラウンド処理
  • 画像の読み込み
  • データベースクエリ
  • サードパーティ製フレームワーク

次の手順:


実際の効果

例:Landmarks アプリ(WWDC 2025 より)

最適化前:

  • お気に入りボタンをタップするたびに、表示中のすべてのランドマークビューが更新されていました
  • 各ビューで距離計算用のフォーマッターが再作成されていました
  • スクロールがぎこちなく感じられました

最適化後:

  • タップしたビューのみが更新されます(細分化されたビュー モデル)
  • フォーマッターは一度だけ作成され、文字列はキャッシュされます
  • 60fps でスムーズにスクロールできます

改善点:

  • 100 回以上の不要なビュー更新 → 1 回の操作につき 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 が遅延なく画面に UI を表示できるよう、処理をすばやく行います
  2. 必要なときだけ更新する — 必要な場合にのみビューが更新されるよう、データフローを設計します
  3. 環境の扱いに注意する — 頻繁に変化する値は保存しないでください
  4. 早い段階から頻繁にプロファイリングする — 問題が発生したときだけでなく、開発中に Instruments を使用します
  5. 最も重要なポイント:優れた SwiftUI のパフォーマンスを実現するには、ビュー本体がすばやく、必要なときだけ更新されるようにします

Xcode: 26+ プラットフォーム: iOS 26+、iPadOS 26+、macOS Tahoe+、axiom-visionOS 3+ 履歴: 変更については git log を参照してください

コメント