debugging-recompositions について
Android Compose 開発者向けの診断スキルです。Layout Inspector とランタイムトレースで、再コンポーズするコンポーザブルと原因を特定します。パラメーターの状態に応じた調査方針を示し、修正自体は扱いません。
主な機能
- 01再コンポーズ数とスキップ数の追跡
- 02パラメーターごとの変更理由の確認
- 03状態に応じた調査方針の提示
- 04リリース版トレースによる確認
活用シーン
- 01スクロールやアニメーション中に再コンポーズする箇所の特定
- 02Changed、Uncertain、Unknown の調査
- 03リリースビルドでの修正確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ skydoves/ compose-performance-skills --skill debugging-recompositionsインストール後、Claude Code で/debugging-recompositionsを入力して読み込まれたか確認します。
詳細
再コンポーズのデバッグ — 見えないものを可視化し、状態をアクションに結び付ける
再コンポーズは、デフォルトでは見えません。アニメーションを再描画するために毎秒60回実行されるコンポーザブルも、親の更新時に一度も再実行されるべきでないコンポーザブルも、ソースコード上では同じように見えます。3つの計測手段で可視化できます。Layout Inspector の再コンポーズ回数とスキップ回数(Android Studio、デバッグビルド)、Layout Inspector の 引数変更理由(Hedgehog 以降、パラメーターごとに Changed/Unchanged/Uncertain/Static/Unknown に分類)、そしてリリース版/R8/実機で計測するための compose-stability-analyzer のランタイム @TraceRecomposition です。
このスキルは診断レイヤーです。再コンポーズを修正するのではなく、どのコンポーザブルが再コンポーズされているか、どのパラメーターが原因かを開発者に示します。パラメーターが特定できたら、修正には関連する別のスキルを使います。型が不安定なら安定性、ラムダのキャプチャなら強力なスキップ、誤ったフェーズでの状態読み取りならフェーズの遅延が対象です。
このスキルを使う場面
- 開発者が「これはスキップされるはずなのに、されない」または「親が更新されるたびに再実行される」と述べている。
- 開発者が特定のコンポーザブルの再コンポーズ回数を数えて、修正が有効だったことを確認したい。
- 開発者が Layout Inspector の再コンポーズ理由パネルに表示される「Uncertain」「Unknown」「Static」「Unchanged」の意味を尋ねている。
- PR で安定性または強力なスキップの修正が主張されているが、レビュー担当者がリリース版で再コンポーズが実際になくなったか確認する必要がある。
- ユーザーが「再コンポーズ回数」「Layout Inspector」「引数変更理由」「Hedgehog」「@TraceRecomposition」または「なぜこれが再コンポーズされるのか」と言及している。
このスキルを使わない場面
- 不安定な型が原因でコンポーザブルが再コンポーズされている場合 — このスキルで原因を特定したら、実際の修正には
../../stability/diagnosing-compose-stability/SKILL.mdと../../stability/stabilizing-compose-types/SKILL.mdを使用します。 - ラムダのキャプチャまたはスキップ不可のスコープが原因でコンポーザブルが再コンポーズされている場合 — 修正には
../using-strong-skipping-correctly/SKILL.mdを使用します。 - 誤ったフェーズ(Composition、Layout、Draw)で状態を読み取ったことが原因でコンポーザブルが再コンポーズされている場合 — 修正には
../deferring-state-reads/SKILL.mdを使用します。 - 今後のリグレッションを防ぐ CI ゲートが必要な場合 —
../../stability/enforcing-stability-in-ci/SKILL.mdを使用します。
前提条件
- 引数変更理由を確認するには Android Studio Hedgehog (2023.1.1) 以降が必要です。それ以前の Studio バージョンでは再コンポーズ回数は表示されますが、パラメーターごとの理由パネルはありません。
- Layout Inspector にはデバッグビルドが必要です(Layout Inspector はデバッグビルドでのみ使用できます)。表示される回数は、あくまで傾向を示す指標として扱ってください。デバッグビルドは Live Literals を有効にしてインタープリター実行されるため、再コンポーズ回数が増加します。
- フローが原因と判明した場合に、通常の
collectAsStateをcollectAsStateWithLifecycleに置き換えられるよう、androidx.lifecycle:lifecycle-runtime-composeが利用可能であること。 - リリースモードで確認するには、
../../measurement/tracing-recompositions-at-runtime/SKILL.mdにあるランタイムトレースの設定が必要です(compose-stability-analyzerランタイムと、Application.onCreate内でのComposeStabilityAnalyzer.setEnabled(true))。 - Compose Compiler のレポート形式に慣れていること(
../../stability/diagnosing-compose-stability/SKILL.mdを参照)。Layout Inspector の分類は、同じ安定性に関する用語を使用しています。
ワークフロー
1. Layout Inspector で再コンポジション数を有効にする
接続されたデバイスまたはエミュレーターで、アプリをデバッグモードで実行します。Android Studio → Tools → Layout Inspector を開きます。Layout Inspector のツールバーで、次の項目を切り替えます。
- Show recomposition counts — コンポーネントツリーに「Recompositions」列を追加し、描画されたツリー上にカウントバッジを重ねて表示します。
- Show recomposition skip counts — コンポーザブルのスキップガードが発動した回数(つまり、呼び出されたものの本体がスキップされた回数)を示す列を追加します。
どちらもデフォルトではオフです。両方をオンにすると、ツリーの各ノードに <再コンポジション数>/<スキップ数> が表示されます。これが主な判断材料になります。
健全な比率は「スキップ数 ≫ 再コンポジション数」です。100/0(再コンポジションが100回、スキップが0回)のコンポーザブルは、問題の決定的な兆候です。親の更新のたびにスキップガードが一度も発動せず、本体が再実行されています。100/95 のコンポーザブルは問題ありません。実際の処理を引き起こした再コンポジションは5回だけです。
2. カウントを確認しながら症状を再現する
カウントは累積されます。症状を再現する前に、Layout Inspector の「Reset」ボタンでリセットします。次に、開発者がジャンクの原因と疑っているユーザー操作(スクロール、タップ、アニメーションの更新など)を行い、どのコンポーザブルのカウントが増えるかを確認します。
カウントが不釣り合いに増加しているコンポーザブルが疑わしい対象です。次に進む前に、その名前とパラメーターのリストを記録してください。
3. 引数の変更理由を確認する
Layout Inspector のツリーで疑わしいコンポーザブルをクリックします。右側のパネルに**「Recomposition reasons」**セクションが表示され、各パラメーターが次の5つの分類のいずれかで示されます。
| ステータス | 意味 | 推奨される対応 |
|---|---|---|
| Static | コンパイル時定数であり、無効化されることはありません。 | なし。このパラメーターは原因ではありません。 |
| Unchanged | 値が前回のコンポジションと等しいと判定されました(同一のインスタンス、または equals が等しい)。 | なし。このパラメーターは再コンポジションを引き起こしていません。 |
| Changed | 値が前回のコンポジションと等しくないと判定されました。 | 変更に意味があったのか、意図しないものだったのかを調べます(例:MutableList フィールドが原因で equals が失敗し、内容が同一なのに copy() されたケース)。 |
| Uncertain | Compose は、前回のコンポジションからパラメーターが変更されたかどうかを判断できません。通常は、パラメーターの型がコンパイラーにとって不安定なため、ランタイムが同一性による比較(===)にフォールバックし、比較結果が決められなかったことを意味します。 | 型を安定化します(@Stable / @Immutable を付与するか、List<T> を ImmutableList<T> / PersistentList<T> に置き換えて構造的な equals が適用されるようにします)。または、再コンポジションを許容します。 |
| Unknown | コンパイラーが型の安定性を静的に分類できませんでした。ランタイムは === にフォールバックしました。 | ../../stability/diagnosing-compose-stability/SKILL.md を実行して、型が分類されない理由を特定します。通常は、@StabilityInferred が付いていない別コンパイル済みモジュール、インターフェース、または Java ライブラリのアノテーションが付いていない POJO が原因です。 |
ステータスを確認して次の対応を決めてください。ステータスを明らかにせずに修正に飛びつかないでください。
4. ステータスをアクションにマッピングする
ステータスを特定した後の判断ツリーは次のとおりです。
- ステータス = Changed。 値が実際に変更されました。その変更が意味のあるものだったかを問いかけてください。ユーザー入力が変わったのか、それとも
copy()が開発者が同一だと考えていた構造的に異なるオブジェクトを生成したのか?よくある原因は、distinctUntilChanged()なしで収集されたFlowが、参照等価性を満たさない等しい値を発行することです。 - ステータス = Uncertain。 同一性は変更されましたが、内容は一致する可能性があります。型にアノテーションを付けるか、安定したコレクションを使用してください。
../../stability/stabilizing-compose-types/SKILL.mdを参照してください。data class copy()後のインスタンス等価性の見落としを示す決定的な証拠であることがよくあります。 - ステータス = Unknown。 コンパイラが分類しませんでした。安定性診断を実行してください。
../../stability/diagnosing-compose-stability/SKILL.mdを参照してください。最も一般的な原因は、インターフェース型のパラメータ、個別にコンパイルされた Java POJO、代入するインスタンスのないジェネリックパラメータです。 - ステータス = Static。 アクションはありません。そのパラメータはコンパイル時定数です。
- ステータス = Unchanged。 アクションはありません。そのパラメータは再コンポジションを通じて等しかったということです。原因は別の場所(別のパラメータ、または親の再起動スコープが無条件に再呼び出ししている)にあるはずです。
5. @TraceRecomposition を使ってリリースで確認する
Layout Inspector のカウントはデバッグビルドから取得されます。デバッグビルドには Live Literals(定数値がゲッターになる)、インタープリタ方式の Compose ランタイム、そして R8 がありません。カウントは方向性を示すシグナルとしては有用ですが、最終的な数値としては使えません。リリースモードでの確認には、疑わしいコンポーザブルを compose-stability-analyzer の @TraceRecomposition で計装し、リリース + R8 ビルドでシナリオを再実行してください。完全なセットアップについては ../../measurement/tracing-recompositions-at-runtime/SKILL.md を相互リンクしています。2 行の要約は次のとおりです。
@TraceRecomposition(traceStates = true)
@Composable
fun SnackRow(snack: Snack, onClick: (Long) -> Unit) { /* ... */ }
Recomposition タグの Logcat は、再コンポジションごとに 1 行を出力し、各パラメータとその変更の有無を示します。リリースモードのカウントが真の値です。修正が機能したと報告する際は、(デバッグのカウントではなく)それを引用してください。
パターン
パターン: デバッグのカウントだけで結論を出さない
# WRONG
"Layout Inspector shows SnackRow at 100/0 in debug — fix shipped."
# WRONG because: debug builds run interpreted with Live Literals turning constants into
# getters that defeat compile-time folding and inflate recomposition. The same composable
# in release + R8 may run 5/95. Always confirm the final number in release with @TraceRecomposition.
# RIGHT
"Layout Inspector showed SnackRow at 100/0 in debug. After the fix, debug shows 5/95
and release-mode @TraceRecomposition confirms 0 recompositions across a 30-frame scroll."
デバッグのカウントがなぜ信用できないかについては、../../measurement/testing-compose-in-release-mode/SKILL.md を参照してください。
パターン: ステータスを名付けてから行動する
# WRONG
"FilterBar is recomposing too much. I'll mark Filter @Stable and see if it helps."
# WRONG because: the developer skipped naming the Argument Change Reason. If the status was
# Unknown, the type needs classification (probably from an external module); if it was
# Uncertain, the type needs @Immutable + ImmutableList; if it was Changed, the upstream
# producer is mutating where it shouldn't. Each requires a different fix; guessing wastes a cycle.
# RIGHT
"FilterBar is recomposing per-frame. Layout Inspector reports the `filter` param as Uncertain.
The Filter type holds List<String>; replacing with ImmutableList<String> + @Immutable should
let equals() catch structural equality. Confirm with @TraceRecomposition in release."
パターン: 「Uncertain」を無視することが最もよくある間違い
「Uncertain」は、単一で最も診断情報が豊富なステータスです。これは次のことを示しています。「コンパイラはこの型を安定していると考え、ランタイムは比較を実行し、同一性が異なっていた」。これはほとんどの場合、copy() またはビルダーが構造的に等しいが同一性の異なるインスタンスを生成しており、さらにサブフィールドが不安定なために equals が適用されないということです。
// WRONG — declared @Immutable but the field is mutable
@Immutable
data class Filter(val tags: List<String>, val sort: SortOrder)
// WRONG because: List<String> is unstable; @Immutable is a contract the developer broke.
// Layout Inspector reports `filter` as Uncertain on every recompose; structural equals
// does not run because a sub-field is unstable.
// RIGHT
@Immutable
data class Filter(val tags: ImmutableList<String>, val sort: SortOrder)
パターン: エンドツーエンドの確認のためにランタイムトレーシングへ連鎖させる
// RIGHT — annotate the suspect, run release with R8, capture logcat
@TraceRecomposition(traceStates = true)
@Composable
fun FilterBar(filter: Filter) { /* ... */ }
adb logcat -s Recomposition:D
# Expected after the fix: a single "[Recomposition #1]" on initial composition,
# then no further entries during the scroll scenario.
完全な計装スキルについては ../../measurement/tracing-recompositions-at-runtime/SKILL.md を参照してください。
パターン: 健全性の指標として再コンポジション数ではなくスキップ数を使う
0/0 のコンポーザブルはデッドコードか、一度も到達されていません。100/0 のコンポーザブルは壊れています。100/95 のコンポーザブルは健全です。親の 100 回のティックのうち実際の再コンポジションはわずか 5 回です。経時的に追跡すべき指標は、絶対的なカウントではなく、スキップ対再コンポジションの比率です。
必須ルール
- 再コンポーズに関する所見を報告する際は、Layout Inspector の Argument Change Reason のステータス(Changed/Unchanged/Uncertain/Static/Unknown)を必ず明記してください。「頻繁に再コンポーズされる」だけでは所見とは言えません。「親のティックごとに、パラメーター
filterのステータスは Uncertain です」のように記述してください。 - デバッグ版の Layout Inspector に表示される再コンポーズ回数を、グラウンドトゥルースの数として扱ってはなりません。デバッグ版では、Compose がインタープリターで実行され、Live Literals も有効になっています。この回数は傾向を示すものにすぎません。
@TraceRecompositionを使ってリリース版で確認してください。 - シナリオを再現する前に、Layout Inspector のカウントを必ずリセットしてください。以前のアプリ状態から累積したカウントが、診断の妨げになります。
- 各ステータスを、対応する正しい関連スキルに必ずマッピングしてください。Uncertain/Unknown →
../../stability/diagnosing-compose-stability/SKILL.md、Changed(ラムダキャプチャ)→../using-strong-skipping-correctly/SKILL.md、Changed(誤ったフェーズでの状態読み取り)→../deferring-state-reads/SKILL.md。 - 「Uncertain」を無視してはなりません。これは、
@Immutableコントラクトに違反する可変サブフィールドを示す、最もよくある決定的な手掛かりです。 - **推奨:**Layout Inspector での所見を
../../measurement/tracing-recompositions-at-runtime/SKILL.mdにつなげ、リリース版でエンドツーエンドの確認を行ってください。 - 推奨:「修正が有効です」と報告する際は、単純な再コンポーズ回数ではなく、スキップと再コンポーズの比率(例:「100/95」または「30 フレームのスクロール中に再コンポーズ 0 回」)を示してください。この比率はデバッグ版とリリース版の間でも変わりませんが、単純な回数は変わります。
通常、再コンポーズは見えません。Layout Inspector、Argument Change Reasons、@TraceRecomposition の 3 つが、診断のためのツール群です。スキップ可能性は KPI ではなく、あくまで診断項目です。目標は「ユーザーが体感するホットパスで予想外の再コンポーズが発生しないこと」であり、「あらゆる場所で再コンポーズをゼロにすること」ではありません。
検証
- Layout Inspector の「Show recomposition counts」と「Show recomposition skip counts」の両方がオンになっている。
- シナリオを再現する前にカウントをリセットした。
- Recompositions 列の増加がスキップ回数に比べて不釣り合いに大きいことから、問題のコンポーザブルを特定した。
- 問題のコンポーザブルについて Argument Change Reasons パネルを確認し、パラメーターごとのステータス(Changed/Unchanged/Uncertain/Static/Unknown)を報告書に明記した。
- ステータスを正しい関連の修正スキルにマッピングし、修正を適用した。該当する場合は、ベースライン(
../../stability/enforcing-stability-in-ci/SKILL.md)も更新した。 - リリース版と R8 ビルドで
@TraceRecompositionを使い、修正後の再コンポーズ回数が開発者の想定と一致することを確認した。デバッグ版の数値は傾向を示すものにすぎません。この数値が最終的な回数です。
参考資料
- Ben Trengrove、「Jetpack Compose:再コンポーズのデバッグ」 — https://medium.com/androiddevelopers/jetpack-compose-debugging-recomposition-bfcf4a6f8d37
- Layout Inspector — https://developer.android.com/studio/debug/layout-inspector
- Compose のパフォーマンス概要 — https://developer.android.com/develop/ui/compose/performance
- Compose の安定性 — 診断 — https://developer.android.com/develop/ui/compose/performance/stability/diagnose
- Ben Trengrove、「Compose のパフォーマンスは常にリリースビルドでテストすべき理由」 — https://medium.com/androiddevelopers/why-should-you-always-test-compose-performance-in-release-4168dd0f2c71
- Chris Banes、「コンポーザブルのメトリクス」 — https://chrisbanes.me/posts/composable-metrics/
- skydoves/compose-stability-analyzer(ランタイムの
@TraceRecomposition) — https://github.com/skydoves/compose-stability-analyzer - skydoves、「Jetpack Compose の 6 つのガイドライン」 — https://medium.com/proandroiddev/6-jetpack-compose-guidelines-to-optimize-your-app-performance-be18533721f9
ステータスが Uncertain または Unknown の場合に安定性を上流で修正する方法については、../../stability/diagnosing-compose-stability/SKILL.md と ../../stability/stabilizing-compose-types/SKILL.md を参照してください。Changed の原因となるラムダのキャプチャについては、../using-strong-skipping-correctly/SKILL.md を参照してください。リリースビルドと R8 ビルドで修正を確認する方法については、../../measurement/tracing-recompositions-at-runtime/SKILL.md と ../../measurement/testing-compose-in-release-mode/SKILL.md を参照してください。今後のリグレッションを防ぐ方法については、../../stability/enforcing-stability-in-ci/SKILL.md を参照してください。