jetpack-compose-performance について
jetpack-compose-performanceは、Jetpack Composeの性能改善を支援するコード監査スキルです。Composable、修飾子、遅延レイアウトのモデルを詳細な規則で点検し、問題の報告や修正前に該当する参考資料を読むよう求めます。
主な機能
- 017つの最適化分野を監査
- 02状態読み取りと安定性を確認
- 03レイアウトの計測と配置を点検
- 04遅延レイアウトのデータモデルを検査
- 05変更前に参考ファイルを必読
- 06リリースビルドとベンチマークを重視
活用シーン
- 01Composeコードの性能問題をレビュー
- 02再コンポーズやスクロールの負荷を調査
- 03レイアウトや状態処理の修正を支援
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ android/ skills --skill jetpack-compose-performanceインストール後、Claude Code で/jetpack-compose-performanceを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/animation.mdeffects-and-threading.mdgraphics-performance.mdlayouts.mdlazy-layouts.mdstate-and-stability.mdreferences/android/develop/ui/compose/custom-modifiers.mdreferences/android/develop/ui/compose/performance/backwards-write.mdreferences/android/develop/ui/compose/performance/stability/strongskipping.md詳細
基本要件
コンポーザブルを細かく最適化する前に、標準的な本番環境向けフラグが設定されていることを確認してください。
- リリースビルド: デバッグ可能なビルドのプロファイリングやベンチマークは行わないでください。ART ランタイムの検証によって、パフォーマンスが大幅に低下します。
- R8 による難読化: バイトコードとコルーチンのディスパッチを最適化するため、R8 を有効にしておいてください。
- ベースラインプロファイル: 重要なユーザージャーニー(コールドスタート、主要なスクロールフィード)のベースラインプロファイルを生成し、ART VM がクラスを事前にコンパイルして読み込めるようにしてください。
最適化監査のワークフロー
- 徹底的なスキャン : 以下の各セクションにある 7 つのコアカテゴリすべてに照らして、対象コードをスキャンしてください。問題が最初に見つかった時点で調査を止めず、各
@Composable関数、修飾子チェーン、遅延レイアウトのデータモデルを、チェックリストのすべてのルールに照らして評価してください。 - 指摘または編集の前に必ずリファレンスを読む : ファイルがカテゴリ 1~7 のルールに該当する場合は、問題を報告したりコードを編集したりする前に必ず、承認済みの修正パターンと適用範囲のすべての制約を確認するため、該当する
references/*.mdファイルを開いて読んでください。要約の箇条書きだけを頼りに修正を書いてはいけません。
1. 状態、スナップショット、安定性(references/state-and-stability.md)
必須 : このカテゴリの問題を指摘または修正する前に、references/state-and-stability.md を必ず読んでください。
- フェーズの遅延(ラムダ):頻繁に変化する状態の読み取りは、ラムダ(
Modifier.offsetやModifier.graphicsLayerなど)でラップし、Composition ではなく Layout または Draw フェーズで読み取られるようにします。 - 逆方向の書き込みを禁止する(読み取りと書き込みの順序を確認する):後続のフェーズで状態を変更したり、同じ Composition パスですでに読み取られた状態を変更したりしないでください(
Read -> Write、またはComposition Read -> Layout or Draw Write)。フェーズ間の逆方向の書き込みを防ぎ、初回フレームのポップ、ジャンク、無限の再コンポーズループを回避します(逆方向の書き込み)。コンポーザブルの先頭での状態の代入(Write -> ReadまたはComposition Write -> Draw Read)は逆方向の書き込みとして報告しないでください。逆方向の書き込みとみなされるのは、書き込みに先立ってその状態が読み取られている場合に限られます。 - Composition で不要な状態ラップを避ける(
Write -> Read):同期値をremember { mutableStateOf(...) }でラップし、Composition の先頭で読み取る前にすぐ上書きするのは避けてください(例:var path by remember { mutableStateOf(Path()) }; path = viewModel.getPath())。逆方向の書き込みではありませんが、不要なスロットテーブルとMutableStateのスナップショットのオーバーヘッドが生じます(また、代入前に読み取りを追加すると逆方向の書き込みになるリスクもあります)。通常のローカルvalを使うか、対象のフェーズ内(Modifier.drawWithCacheなど)で値を直接読み取ってください。 rememberのキーを明示する & スロットのオーバーヘッド:remember内で参照するすべての入力変数とコンポーザブルのパラメーターを計算キーとして必ず渡してください。rememberはスロットテーブルにkeys + 1個のオブジェクトを格納し、再コンポーズのたびにキーの等価性を確認します。割り当てのコストが高い場合、またはオブジェクトの同一性を維持する必要がある場合にのみrememberを使ってください。derivedStateOfをラップする際は、スナップショット状態の値やプロパティ(例:state.value)をキーとして渡さないでください(Stateインスタンス自体は、そのインスタンス参照が変わる可能性がある場合は渡してかまいません)。computedStateOfとderivedStateOfの使い分け(Compose 1.13+):Compose 1.13 以降では、変化の速い入力からの単純な条件チェックや状態のフィルタリング(scrollState.value > 0など)にはcomputedStateOfを使います。derivedStateOfより無効化が大幅に高速です(Compose< 1.13ではremember { derivedStateOf { ... } }を使ってください)。マルチリードのキャッシュが有効な重い計算や、無効化を挟まずに Composition ごとに十分な頻度(およそ10 / numberOfStatesReadPerCalculation回)で読み取られ、無効化コストを補える単純な計算に限りderivedStateOfを使います。- 同期的な状態の導出:Compose の状態(スクロール位置やコレクションのサイズなど)を監視する非同期の
produceStateとsnapshotFlowのブロックを確認し、同期的なremember { derivedStateOf { ... } }(Compose 1.13 以降ではcomputedStateOf)に置き換えてください。 - 単純なプリミティブ:単純なブール式やプロパティの参照には、
derivedStateOfやcomputedStateOfより通常のカスタム Kotlin ゲッター(get())を優先してください。 - コレクション状態の追跡:リスト全体の更新には、
mutableStateListOf()ではなく、通常のmutableListOf()とmutableStateOf(list, neverEqualPolicy())を使い、更新を支える代入を行ってください。 - スナップショットコレクション:繰り返しクエリや反復処理を行う際は、要素ごとの読み取り監視のオーバーヘッドを避けるために
SnapshotStateList.toList()を使ってください。 - 状態の繰り返し読み取り:Compose の
Stateプロパティを繰り返し読み取る場合は、ブロックの先頭でローカル変数に取り出してください。 - セッター内の状態:カスタムプロパティセッター内で Compose の
Stateを読み取るのは避けてください。変更に伴う副作用を発生させる場合は生のバッキングフィールドを使い、複数のプロパティの更新はまとめて行ってください。 - 安定性とスキップのトレードオフ(スキップは無料ではありません):安定性にこだわりすぎたり、
@Immutable、@Stable、ImmutableListをデフォルトの解決策として扱ったりしないでください。コンポーザブルのスキップは無料ではありません。各パラメーターは記憶された値と同様にスロットテーブルに格納され、再コンポーズのたびにすべての入力を比較する必要があります。強力なスキップモードでは、インスタンスの等価性(===)が一致すれば、不安定なパラメーター(標準のkotlin.collections.Listを含む)を持つコンポーザブルもすでにスキップされます。モデルにアノテーションを付けたりImmutableListを使ったりすると構造的な等価性(.equals())が強制され、大規模なコレクションや複雑なクラスでは、再コンポーズ自体より大幅にコストが高くなる場合があります。 - コレクションを含むモデルに
@Stable、@Immutable、ImmutableListを追加しない:コードの作成や最適化を行う際、コレクション(List、Set、Map)を含むモデルに@Immutableや@Stableを追加しないでください。また、kotlin.collections.List<T>をImmutableList<T>に置き換えること(またはstability_config.txtにImmutableListを追加すること)を決して推奨しないでください。強力なスキップモードでは、コレクションに対するO(N)の.equals()呼び出しを再コンポーズのたびに行うほうが、O(1)のインスタンスの等価性(===)よりもコストが高くなります。 - 既存の
@Stable/@Immutableアノテーションを削除する際の適用範囲のガード (大規模な遅延データセットのみ):- 削除する場合:次の両方の条件を満たす場合に限り、データモデルから既存の
@Stableまたは@Immutableアノテーションを削除するよう推奨してください。(1)そのモデルが、遅延レイアウト(LazyColumn、LazyRow、LazyGrid、Pager、ScalingLazyColumn)内で入力フィードまたはアイテムモデルとして使われている。かつ(2)スクロールや再コンポーズのパフォーマンスを低下させる再帰的なO(N)の.equals()チェックが生じる、大規模なデータセットまたは深くネストしたコレクション階層(フィードのストリーム、大量のタグ/アイテムを含むリスト、List<Paragraph>とList<Markup>を持つPostのような多階層のネストリストなど)を含んでいる。 - そのままにする場合:それ以外の既存の
@Stable、@Immutable、ImmutableListの使用箇所(遅延 UI 以外のモデル、単独のコンポーネント、または少数のバッジ、サムネイル、メタデータの行を持つカードなど、データセットのサイズが小さいか上限が定まっているモデルなど)は、コードベース内でそのままにし、問題として報告しないでください。大規模なデータセットを持つ遅延レイアウトに渡されていることを確認せず、コードベース全体の@Stable/@Immutableクラスを無差別に検索して問題として報告しないでください。
- 削除する場合:次の両方の条件を満たす場合に限り、データモデルから既存の
- 強力なスキップモードで
@Stable/@Immutableを追加する場合:Compose モジュール内のプリミティブ型やStringのvalプロパティを持つフラットなdata classは、コンパイラーによってすでに安定と推論され、.equals()が自動的に使われます。これらに冗長な@Immutableアノテーションを追加しないでください。コンパイラーが安定性を推論できない場合(Compose 以外のモジュールのクラスやインターフェースなど)に限り、データソース(Room や MVI reducer など)が変更のないアイテムに対してもエミッションのたびに新しいオブジェクトインスタンスを割り当てる場合は、クラスが小さく、フラットで、コレクション(List、Set、Map)を含まず、.equals()チェックが単純なO(1)であることを条件として、@Stableまたは@Immutableを追加してください。 - スキップを無理に強制するのではなく、コンポーザブルの実行コストを下げる:再コンポーズの回数に過度にこだわらないでください。再コンポーズが実行中のパスでスキップを行っても、Composition がスケジュールされた後はほとんど時間を節約できません。コストの大部分は、再コンポーズのパスのスケジューリングと開始にかかります。スキップでパフォーマンスが向上すると決めつけず、コンポーザブルの実行コストを下げ、状態の読み取りを Layout/Draw に遅延し、ベンチマークで検証することに注力してください。
2. レイアウトと計測の最適化(references/layouts.md)
必須:このカテゴリの問題を指摘または修正する前に、必ず references/layouts.mdをお読みください。
- 座標と配置:
onGloballyPositionedは避けてください。ビューポートの追跡にはModifier.onVisibilityChanged、ローカル境界の取得にはModifier.onPlacedまたはModifier.onLayoutRectChangedを使用し、Modifier.Nodeのイベントハンドラー内で必要に応じてrequireLayoutCoordinates()を呼び出してください。 - カスタム
Layoutによるフローティングコンテンツのアンカー設定:コンポーネントにフローティングラベルやツールチップを付ける場合は、両方の子要素を相互に相対配置するカスタムLayoutを使用してください。onGloballyPositionedの座標をMutableStateで追跡し、offsetでフローティングコンテンツを配置する方法は避けてください。これにより1フレームの遅延が発生し、2回目のCompositionパスがスケジュールされます。 - オフセットのフェーズ遅延:動的なオフセットはラムダでラップし(
Modifier.offset { IntOffset(...) })、Compositionをスキップして直接Layoutに進むようにしてください。 - ツリーの深さと装飾用ラッパー:冗長なラッパー
Boxや装飾用のCanvasノードは避けてください。モディファイアを直接付けるか、Modifier.drawBehindを使用してください。 - サブコンポジションのオーバーヘッド:ホットパスや遅延アイテム内で
BoxWithConstraints/SubcomposeLayoutを避けてください。標準のモディファイア、Modifier.layout、またはLayoutModifierNodeを優先してください。 - 固有計測:固有計測は慎重に使用し、深くネストされた固有計測のチェーンは避けてください。
3. 遅延レイアウトの最適化(references/lazy-layouts.md)
必須:このカテゴリの問題を指摘または修正する前に、必ず references/lazy-layouts.mdを読んでください。
- 遅延レイアウトの適用範囲のガード:
keyとcontentTypeに関する指示は、遅延コンテナ(LazyColumn、LazyRow、LazyGrid)にのみ厳密に適用されます。コレクションを反復処理する標準的な非遅延コンテナ(Column、Row、Box)を指摘しないでください。 contentTypeとプリミティブ型のkeyを必ず指定:遅延レイアウト(LazyColumn、LazyRow、LazyGrid)のすべてのアイテムに、プリミティブ型(Int、Long、String)のkeyとcontentTypeの両方を必ず指定してください。itemsまたはitemsIndexedを使用する場合は、contentTypeを省略しないでください:contentType = { _, item -> item.type }。contentTypeを省略すると、スクロール中にComposeがコンポジションスロットを再利用できなくなります。- アイテム内のインデックス検索:レイアウトパス中の
O(N²)の検索をなくすため、list.indexOf(item)をitemsIndexedに置き換えてください。 - 監視対象となる選択状態と状態クエリ:選択状態の追跡には、
mutableStateListOfではなく、MutableState内の不変なSet(mutableStateOf(emptySet<String>()))、mutableStateSetOf()、またはO(1)で検索できるMapを使用してください。ラムダプロバイダー(isSelected: () -> Boolean)をリーフコンポーザブルに渡し、親のitemsスコープからの再コンポジションを切り離してください。 - 遅延アイテム内へのCompositionLocalsの伝播:個々の遅延アイテムのコンポーザブル内でCompositionLocal(
LocalContext.current、LocalDensity.current、LocalSharedTransitionScope.current、スコープなど)を参照するのは避けてください。親コンテナまたは画面内で一度だけ解決し、解決した値をパラメーターとして下位に渡してください。 - 遅延アイテム内の共有トランジションを遅延:遅延アイテムの
Modifier.sharedBoundsまたはModifier.sharedElementは、可視性インジケーター(Modifier.onVisibilityChangedやビューポートの可視性チェックなど)を使って、表示領域外や事前取得されたアイテムがトランジションスコープをインスタンス化しないようにしてください。 - アイテム数の導出:コレクションの件数を
derivedStateOfでラップせず、.sizeを直接読み取ってください。 - 小規模で上限のあるリスト:小規模または上限のあるコレクション(例:1~10個のアイテム)には、
forまたはforEachループを使う標準的なRowまたはColumnを使用し、遅延仮想化、測定、事前取得のオーバーヘッドをなくしてください。 - 共有Flowのホイスト:共有
Flowは遅延レイアウトより前の親コンテナで一度だけ収集してください。表示中の各アイテムでcollectAsStateWithLifecycle()を呼び出さないでください。 - 事前取得アイテムでの副作用を回避:遅延アイテム内の
LaunchedEffect(Unit)でアナリティクスを呼び出さないでください。代わりに、滞在時間のしきい値を設定したModifier.onVisibilityChangedを使用してください。
4. アニメーションと共有トランジション(references/animation.md)
必須:このカテゴリの問題を指摘または修正する前に、必ず references/animation.mdを読んでください。
- アニメーションプロパティのフェーズ遅延:変化の速いアニメーション状態の読み取り(alpha、scale、rotation、color)は、
Modifier.graphicsLayer { ... }でラップし、CompositionをバイパスしてDrawフェーズで直接評価してください。 - オフセットとサイズのフェーズ遅延:アニメーション化されたオフセットは
Modifier.offset { IntOffset(...) }でラップし、制約はModifier.layout { ... }またはModifier.animateBoundsでアニメーション化して、サイズ遷移中の再コンポジションを回避してください。 - 高速スクロールリストでの共有トランジションの遅延:破棄される事前取得アイテムでのModifierと監視処理のインスタンス化コストを避けるため、表示中に限り
Modifier.onVisibilityChangedを使ってModifier.sharedBoundsまたはModifier.sharedElementをアタッチしてください。 - ラムダプロバイダー:生のアニメーション値ではなく、ラムダプロバイダー(
() -> Float)を子コンポーザブルに渡し、状態の読み取りをDrawまたはLayoutまで遅延させてください。 - Node内の永続的な
Animatable:AnimatableをModifier.Nodeの永続的なメンバープロパティとして保存し、onAttach()でcoroutineScope.launchを使って起動してください。
5. 副作用、ライフサイクル、スレッド処理(effects-and-threading.md)
必須:このカテゴリの問題を指摘または修正する前に、必ず references/effects-and-threading.md をお読みください。
- 本体での直接的な副作用を禁止:コンポジション中にComposableの本体で直接副作用を実行しないでください。
SideEffect、LaunchedEffect、またはユーザーイベントのコールバックでラップしてください。 - 非サスペンドの
LaunchedEffectと空のDisposableEffectを禁止:同期処理(ビューのログ記録やオブジェクトの更新など)にLaunchedEffectを使用しないでください。回避策として、空のonDispose {}ブロックを含むDisposableEffectを使用しないでください。代わりにSideEffectを使用してください(Compose 1.12以降では、SideEffect(keys) { ... }でキーを直接指定できます。Compose< 1.12では、SideEffectに引数を渡せないため、プロジェクトのComposeバージョンを確認し、rememberしたホルダーをSideEffect { ... }内で使用してキーの変更を追跡してください)。 - 本体内の割り当てをキャッシュ:日付フォーマッターや複雑なパスなど、負荷の高いオブジェクトの割り当てや高コストな計算は
rememberでラップしてください。 - 関連する
LaunchedEffectを統合:複数のLaunchedEffect(Unit)呼び出しを、ネストしたlaunch { ... }ブロックを含む単一のLaunchedEffectに統合し、エフェクトノードの割り当てとコルーチンランチャーのオーバーヘッドを削減してください。 - エフェクトの順序と終了処理のコスト:コンポジションの変更が適用される際のエフェクト実行順序を理解してください(
DisposableEffectが最初に実行され、次にSideEffectが実行されます。一方、LaunchedEffectはコンポジションのCoroutineContext上でコルーチンを起動し、非同期にディスパッチされるため、Drawの後まで待機する保証はありません)。LaunchedEffectのキャンセルにはコルーチンのキャンセルと終了処理のオーバーヘッドが発生することに注意してください。アニメーションやスクロール中にLaunchedEffectのキーを頻繁に切り替えることは避けてください。 - メインスレッドの処理をオフロード:ブロッキングI/O、負荷の高いファイル解析、データベース操作は、
produceStateを使ってwithContext(Dispatchers.IO)にオフロードしてください。 - 遅延レイアウトでアイテム単位のインプレッションエフェクトを使用しない:インプレッションのログ記録に、遅延アイテム内で
LaunchedEffectまたはDisposableEffectを使用しないでください。遅延アイテムはビューポートに入る前に事前コンポーズされるため、インプレッションが早まったり、誤って記録されたりします。代わりに、滞在時間のしきい値を設定してModifier.onVisibilityChangedを使用してください。 - ライフサイクル対応のFlow収集:Android上でバックグラウンド処理を継続的に生成するUIのFlow収集には
collectAsStateWithLifecycle()を使用し、軽量なインメモリのStateFlowや非AndroidのスコープにはcollectAsState()を使用してください。 - レシーバーとリスナーを親に移動:システムリスナーと
BroadcastReceiverは、遅延リストアイテム内ではなく、画面レベルでDisposableEffectを使って一度だけ登録してください。 - エフェクトを条件でガード:空のコルーチンやエフェクトノードの割り当てを防ぐため、起動前に
if (callback != null)でエフェクトをガードしてください。 - 古いキャプチャを防止:
LaunchedEffect内の長時間実行またはループするコルーチンがコールバックやパラメーターを参照する場合は、rememberUpdatedStateを使用してください。 - 古いタイマーを置き換え:
Handler.postDelayedまたはTimerTaskをLaunchedEffectとdelay()に置き換えてください。
6. グラフィックスとカスタム描画(references/graphics-performance.md)
必須:このカテゴリの問題を報告または修正する前に、必ず references/graphics-performance.md をお読みください。
- 描画割り当てのキャッシュ:装飾用の
CanvasノードをModifier.drawWithCacheに置き換え、Path、Paint、Brushの割り当てをキャッシュします。 - Image ではなく AsyncImage を使用:インターネットから画像を読み込む場合は Coil の
AsyncImageを、ドローアブルにはpainterResourceを使用します。Imageは画像のキャッシュと読み込みに最適化されていないため、パフォーマンスの問題が発生する可能性があります。 - 頻繁に変化するユニフォームのスコープ設定:コンポジション内で
RuntimeShaderとShaderBrushをrememberし、サイズのユニフォームはdrawWithCacheで、フレームごとのユニフォームはonDrawBehindで更新します。 - グラフィックスパスオブジェクトの再利用:描画のホットループでは、再割り当てせずに割り当て済みバッファーを保持できる
reset()よりPath.rewind()を優先します。 - 値クラスとプリミティブコレクション:汎用タプル(
Pair、Triple)と汎用セット(HashSet<Long>)は避け、androidx.collection.MutableLongSetとandroidx.compose.ui.util.packIntsを使用します。
7. カスタムモディファイア:Modifier.Node(カスタムモディファイアガイド)
-
Modifier.Node への移行:レガシーの
Modifier.composedをModifierNodeElement+Modifier.Node(または、SuspendingPointerInputModifierNodeなどのサブノードに委譲する場合はDelegatingNode)に移行し、機能インターフェース(DrawModifierNode、LayoutModifierNode、SemanticsModifierNode、currentValueOf(...)を使用するCompositionLocalConsumerModifierNode)を利用して、ノードごとのサブコンポジションのオーバーヘッドをなくし、 ノードの再利用を可能にします(Modifier.Node を使用したカスタムモディファイアの作成を参照):// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color) // ModifierNodeElement private data class CircleElement( val color: Color ) : ModifierNodeElement<CircleNode>() { override fun create() = CircleNode(color) override fun update(node: CircleNode) { node.color = color } } // Modifier.Node private class CircleNode( var color: Color ) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
-
必要に応じた座標の取得:
Modifier.Nodeでは、GlobalPositionAwareModifierNode.onGloballyPositionedを実装するのではなく、ポインターまたは描画ハンドラー内で必要に応じてrequireLayoutCoordinates()を照会します。
重要:一般的なモディファイアを Modifier.Node に移行する例については、Modifier.Node を使用したカスタムモディファイアの作成を必ずお読みください。