swiftui-performance について
SwiftUI 開発者が再現可能な性能問題を調査し、コードや Instruments のトレースを根拠に診断・検証するためのスキルです。測定せず広範囲に変更するのではなく、対象を絞った修正を扱います。
主な機能
- 01コードレビューと性能問題の切り分け
- 02Instruments のトレース取得と分析
- 03状態依存関係とビュー ID の調整
- 04遅延読み込みと監視範囲の調整
- 05基準値との比較と修正の検証
活用シーン
- 01リスト更新やスクロールの引っかかりを調査
- 02ビュー更新が頻発する原因を特定
- 03メインスレッド上の負荷の高い処理を分析
- 04変更前後の性能指標を比較
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill swiftui-performanceインストール後、Claude Code で/swiftui-performanceを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/evals.jsonreferences/demystify-swiftui-performance-wwdc23.mdoptimizing-swiftui-performance-instruments.mdunderstanding-hangs-in-your-app.mdunderstanding-improving-swiftui-performance.mdwwdc-session-sources.md詳細
SwiftUI のパフォーマンス
再現可能な症状から、測定に基づく改善まで、SwiftUI ビューのパフォーマンスを監査します。アニメーションの設計は swiftui-animation、本番環境のテレメトリーは metrickit、所有権やリークの分析は ios-memgraph-analysis、ナビゲーションの動作は swiftui-navigation、状態アーキテクチャは swiftui-patterns、レイアウトの構築は swiftui-layout-components に案内してください。
目次
- ワークフローの判断ツリー
- 1. コードを優先したレビュー
- 2. プロファイリングの案内
- 3. 分析と診断
- 4. 改善
- よくあるコードの問題(と修正方法)
- 5. 検証
- 成果物
- Instruments によるプロファイリング
- ID とライフタイム
- 遅延読み込みのパターン
- 状態と Observation の最適化
- よくある間違い
- レビューのチェックリスト
- 参考資料
ワークフローの判断ツリー
- コードが提供された場合:まずレビューし、指摘事項を仮説として示します。
- 症状のみの場合:関連性の高いビュー、データフロー、再現手順、デバイス、OS、ビルド構成を必要最小限の範囲で収集します。
- レビューで結論が出ない場合:大規模なリファクタリングを提案する前に、トレースまたはレーンのスクリーンショットを収集します。
コードとトレースの分析の両方で、次のトリアージリストを使用します。
- 状態への広範な依存や無効化の頻発
- 不安定なリストの ID や、ルートビューの条件による切り替え
body内でのフォーマット、ソート、デコード、同期 I/O- レイアウト/ジオメトリのフィードバックループや、大きすぎる画像
- 大規模な階層に適用された暗黙のアニメーション
1. コードを優先したレビュー
トリアージリストにある各懸念事項を、該当するコードに対応付けます。コード参照を示して考えられる原因を報告しますが、トレースでコストが確認されるまでは、コードに基づく仮説として明示します。根拠が不足している場合は、最小限の再現手順または測定方法を提案します。
2. プロファイリングの案内
可能であれば、実機で Release ビルドに対して SwiftUI Instruments テンプレートを使用します。該当する場合は SwiftUI のレーン、Time Profiler、Hangs/Hitches を記録しながら、問題の操作を正確に再現します。トレース、またはレーンとコールツリーのスクリーンショットを依頼します。
3. 分析と診断
トレースの証拠に対して、同じトリアージリストを適用します。長時間または頻繁に発生する SwiftUI の更新を、Time Profiler のコールツリーおよび再現した操作と照合します。トレースに裏付けられた指摘と、コードに基づく仮説を区別し、残る不確実性を解消する次の測定方法を明示します。
4. 改善
対象を絞った修正を適用します。
- 状態の適用範囲を狭めます(
@State/@Observableを子ビューに近づけます)。 ForEachとリストの ID を安定させます。- 負荷の高い処理を
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(_:) の変更は意図的なリセットに限ります。プロファイリング対象のホットな行では、AnyView よりも @ViewBuilder またはジェネリックなコンポジションを優先してください。証拠から状態の頻繁な変化や高コストな再生成が確認できる場合は、ルートの条件分岐を疑います。ただし、自動的に欠陥とみなす必要はありません。
Text(title)
.foregroundStyle(isHighlighted ? .yellow : .primary)
ForEach(items) { item in
Row(item: item).id(item.stableID)
}
遅延読み込みのパターン
プロファイリングで、先行生成、レイアウト、または更新の処理が無視できないコストだと分かった場合は、遅延コンテナを使います。項目数に一律のしきい値はありません。グリッド/リストの構築方法については、swiftui-layout-components を参照してください。
注意点:
- 画面外のビューは遅延スタックから取り除かれます。SwiftUI はそれらを一時的に保持する場合がありますが、その後ビューとビュー内の状態を削除します。
- スクロールして表示範囲から外れても重要な行の状態を保持する必要がある場合は、行ビューの外部に状態を保持してください。
- プリフェッチにより、
onAppearより前にbodyやレイアウトの処理が行われる場合があります。行の描画に必要なデータのセットアップをonAppearだけに依存させないでください。 onAppearとonDisappearは可視性を示すシグナルとして扱い、ライフタイムを保証するものとはみなさないでください。ForEachの前にデータをフィルタリングしてください。各要素から行が 0 個または 1 個生成されるようなif分岐は避けてください。- 各
ForEach要素内のトップレベルのサブビュー数は一定にしてください。必要に応じて、行の内容を安定したコンテナで囲みます。リスト/テーブルの遅い経路をデバッグする際は、-LogForEachSlowPath YESを使ってください。 - コンテンツサイズやコンテンツオフセットが絶対値であるという前提は避けてください。遅延スタックは画面外のサイズを推定します。
- 遅延行でジオメトリのフィードバックループを避けてください。ジオメトリの変更を行の状態に反映する前に、安定したサイズ設定、レイアウトプリミティブ、またはカスタム
Layoutを優先してください。
状態と Observation の最適化
Observation は、ビューの評価中に読み取られたプロパティを追跡します。必要最小限の派生値を渡すか、対象を絞った子ビューに読み取りを移して、更新の波及範囲を減らしてください。
// Split reads into child views so each tracks only what it renders.
struct ProfileView: View {
let model: ProfileModel
var body: some View {
VStack {
NameRow(model: model) // only tracks name
EmailRow(model: model) // only tracks email
AvatarView(model: model) // only tracks avatar
SettingsForm(model: model) // only tracks settings
}
}
}
安価な計算値は派生値のままでかまいません。高コストな変換には、明示的な所有者、入力セット、更新トリガーが必要です。パフォーマンス対策を目的に機械的にビューモデルを追加しないでください。まず計測し、一般的な状態設計については swiftui-patterns を参照してください。
よくある間違い
- Debug ビルドでプロファイリングする。 Debug ビルドには追加のランタイムチェックが含まれ、最適化が無効になっているため、実際のパフォーマンスを反映しないデータが得られます。実機で Release ビルドをプロファイリングしてください。
- 必要なプロパティが1つだけなのに、モデル全体を監視する。 大きな
@Observableモデルは、目的ごとに絞ったモデルに分割するか、計算プロパティやクロージャーを使って監視範囲を狭めてください。 - ScrollView の項目内でジオメトリのフィードバックを使う。 GeometryReader や頻繁に変化するジオメトリの状態は、レイアウトを繰り返し実行させることがあります。安定したサイズ指定、カスタムレイアウト、またはしきい値を設けた、範囲を絞った
.onGeometryChange(iOS 16 以降)を使用してください。 body内でDateFormatter()やNumberFormatter()を呼び出す。 これらの生成にはコストがかかります。static にするか、ビューの外に移動してください。- Equatable ではない状態をアニメーションする。 SwiftUI が等価性を判定できない場合、フレームごとに再描画します。状態を
Equatableに準拠させたうえで、値に応じた単純な変更には.animation(_:value:)を、修飾子の範囲を絞った暗黙的なアニメーションには.animation(_:body:)を使ってください。 - 識別子のない、大きくフラットな
Listを使う。id:を指定するか、項目をIdentifiableに準拠させてください。そうすれば、リスト全体を再構築する代わりに、SwiftUI が効率的に差分を検出できます。 - 不要な
@Stateラッパーオブジェクトを使う。@Stateのために単純な値型をクラスでラップすると、値セマンティクスが損なわれます。構造体と通常の@Stateを使ってください。 - 同期 I/O で
MainActorをブロックする。 ファイルの読み込み、大きなペイロードの JSON 解析、画像のデコードは、メインアクター以外で行う必要があります。nonisolated な非同期ヘルパーや専用のアクターを優先してください。Task.detachedは、意図的にアクターの継承を解除し、キャンセルを自分で処理する場合に限って使ってください。
レビューチェックリスト
-
body内でDateFormatter/NumberFormatterを生成していない - 大きなリストで
Identifiableに準拠した項目、または明示的なid:を使用している -
@Observableモデルが、ビューで実際に読み取るプロパティだけを公開している - 負荷の高い計算(画像処理、解析)を
MainActor以外で実行している - 遅延読み込みされる行の ID が安定しており、最上位の行の構造が一定で、データが事前にフィルタリングされている
- スクロール行のジオメトリの変化にしきい値を設け、広範囲の状態に影響を与えない
- 行の描画で、唯一のセットアップ箇所として
onAppearに依存していない - 暗黙的なアニメーションでは、値に応じた変更に
.animation(_:value:)を、修飾子の範囲を絞る場合に.animation(_:body:)を使用している - メインスレッドで同期的なネットワーク I/O/ファイル 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