compose-performance-audit について
compose-performance-auditは、Android開発者向けのJetpack Compose性能監査スキルです。コードレビューからプロファイリングデータの分析、修正案まで扱い、コードだけで判断できない場合はLayout InspectorやPerfettoのデータ収集を案内します。
主な機能
- 01再コンポーズ、状態、パラメーターの安定性を確認
- 02遅延リストのキーとコンポジション中の処理を点検
- 03Layout InspectorとPerfettoのデータを分析
- 04対象を絞った修正案を提示
- 05ベンチマークとフレーム時間を比較
活用シーン
- 01Compose UIのカクつきを調査
- 02LazyColumnのキーによる再コンポーズを確認
- 03コンポジション中の高コスト処理を特定
- 04性能トレースで最適化結果を検証
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ new-silvermoon/ awesome-android-agent-skills --skill compose-performance-auditインストール後、Claude Code で/compose-performance-auditを入力して読み込まれたか確認します。
詳細
Compose のパフォーマンス監査
概要
計測とベースラインの設定から、根本原因の分析、具体的な改善手順まで、Jetpack Compose のビューのパフォーマンスをエンドツーエンドで監査します。
ワークフローの判断ツリー
- ユーザーがコードを提示した場合は、「コード優先レビュー」から始めます。
- ユーザーが症状のみを説明した場合は、必要最小限のコードやコンテキストを尋ねてから、「コード優先レビュー」を行います。
- コードレビューで結論が出ない場合は、「プロファイリングの案内」に進み、Layout Inspector の出力または Perfetto のトレースを依頼します。
1. コード優先レビュー
収集する情報:
- 対象の Composable コード。
- データフロー:状態、remember、派生状態、ViewModel との接続。
- 症状と再現手順。
注目する点:
- 不安定なパラメーターや広範囲な状態変更による再コンポジションの嵐。
LazyColumn/LazyRowの不安定なキー(keyの頻繁な変化、キーの欠落)。- コンポジション中の重い処理(書式設定、ソート、フィルタリング、オブジェクトの割り当て)。
- 不要な再コンポジション(
rememberの不足、不安定なクラスやラムダ)。 - 適切なサイズ指定や非同期読み込みがない大きな画像。
- レイアウトの過剰な再計算(深いネスト、intrinsic measurement、
SubcomposeLayoutの誤用)。
提示する内容:
- コードの該当箇所を示した、考えられる根本原因。
- 推奨する修正とリファクタリング。
- 必要に応じて、最小限の再現例または計測の提案。
2. プロファイリングの案内
データの収集方法を説明します:
- Android Studio の Layout Inspector を使い、再コンポジションの回数を確認します。
- Compose ツールで Recomposition Highlights を有効にします。
- フレームタイミングの分析には Perfetto または System Trace を使います。
- 起動やスクロールの指標について Macrobenchmark の結果を確認します。
依頼するもの:
- 再コンポジションの回数が表示された Layout Inspector のスクリーンショット。
- Perfetto のトレースまたは System Trace のエクスポートデータ。
- デバイス/OS/ビルド構成(デバッグ版かリリース版か)。
重要:R8 を有効にしたリリースビルドでプロファイリングを行ってください。デバッグビルドには大きなオーバーヘッドがあります。
3. 分析と診断
Compose でよく見られる原因を優先して調べます:
- 不安定なパラメーターや広範囲な状態変更による再コンポジションの嵐。
- 遅延リストの不安定なキー(
keyの頻繁な変化、インデックスベースのキー)。 - コンポジション中の重い処理(書式設定、ソート、オブジェクトの割り当て)。
- 再コンポジションのたびに再生成される原因となる**
rememberの不足**。 Modifier.size()による制約がない大きな画像。- 不適切なコンポジションフェーズでの不要な状態読み取り。
トレース/Layout Inspector の証拠に基づいて、調査結果をまとめます。
4. 改善
対象を絞った修正を適用します:
- パラメーターを安定化する:データクラスに
@Stableまたは@Immutableアノテーションを使います。 - キーを安定化する:
LazyColumn/LazyRowのアイテムには、安定した一意の ID を使います。 - 状態の読み取りを遅らせる:
derivedStateOf、ラムダベースの Modifier、またはModifier.drawBehindを使います。 - コストの高い計算を記憶する:
remember { }またはremember(key) { }でラップします。 - 再コンポジションをスキップする:安定した Composable を抽出し、
key()で識別性を制御します。 - 画像を非同期で読み込む:適切なサイズ制約とともに Coil/Glide を使います。
- レイアウトの複雑さを軽減する:階層を平坦化し、深いネストを避けます。
よくあるコードの問題(と修正方法)
不安定なラムダキャプチャ
// BAD: New lambda instance every recomposition
Button(onClick = { viewModel.doSomething(item) }) { ... }
// GOOD: Use remember or method reference
val onClick = remember(item) { { viewModel.doSomething(item) } }
Button(onClick = onClick) { ... }
コンポジション中のコストの高い処理
// BAD: Sorting on every recomposition
@Composable
fun ItemList(items: List<Item>) {
val sorted = items.sortedBy { it.name } // Runs every recomposition
LazyColumn { items(sorted) { ... } }
}
// GOOD: Use remember with key
@Composable
fun ItemList(items: List<Item>) {
val sorted = remember(items) { items.sortedBy { it.name } }
LazyColumn { items(sorted) { ... } }
}
LazyColumn のキーの欠落
// BAD: Index-based identity (causes recomposition on list changes)
LazyColumn {
items(items) { item -> ItemRow(item) }
}
// GOOD: Stable key-based identity
LazyColumn {
items(items, key = { it.id }) { item -> ItemRow(item) }
}
不安定なデータクラス
// BAD: Unstable (contains List, which is not stable)
data class UiState(
val items: List<Item>,
val isLoading: Boolean
)
// GOOD: Mark as Immutable if truly immutable
@Immutable
data class UiState(
val items: ImmutableList<Item>, // kotlinx.collections.immutable
val isLoading: Boolean
)
状態の早すぎる読み取り
// BAD: State read during composition (recomposes whole tree)
@Composable
fun AnimatedBox(scrollState: ScrollState) {
val offset = scrollState.value // Recomposes on every scroll
Box(modifier = Modifier.offset(y = offset.dp)) { ... }
}
// GOOD: Defer state read to layout/draw phase
@Composable
fun AnimatedBox(scrollState: ScrollState) {
Box(modifier = Modifier.offset {
IntOffset(0, scrollState.value) // Read in layout phase
}) { ... }
}
コンポジション中のオブジェクト割り当て
// BAD: Creates new Modifier chain every recomposition
Box(modifier = Modifier.padding(16.dp).background(Color.Red))
// GOOD for dynamic modifiers: Remember the modifier
val modifier = remember { Modifier.padding(16.dp).background(Color.Red) }
Box(modifier = modifier)
安定性チェックリスト
| 型 | デフォルトで安定しているか? | 修正方法 |
|---|---|---|
プリミティブ(Int、String、Boolean) | はい | 該当なし |
安定したフィールドを持つ data class | はい* | すべてのフィールドが安定していることを確認する |
List、Map、Set | いいえ | kotlinx の ImmutableList を使う |
var プロパティを持つクラス | いいえ | 外部から見て安定している場合は @Stable を使う |
| ラムダ | いいえ | remember { } を使う |
5. 検証
ユーザーに以下を依頼します。
- Layout Inspector を再実行し、再コンポジション回数を比較します。
- Macrobenchmark を実行し、フレームタイミングを比較します。
- リリースビルドを実際のデバイスでテストします。
情報が提供されている場合は、差分(再コンポジション回数、フレーム落ち、ジャンク)をまとめます。
出力
以下を提示します。
- 簡潔なメトリクス表(可能な場合は変更前/変更後)。
- 影響の大きい順に並べた主な問題。
- 推定作業量を添えた修正案。
参考資料
コメント
Markdown 対応