compose-stability-diagnostics について
Jetpack Compose 開発者向けに、コンパイラレポートと実行時の証拠を使って不要な再コンポーズを調べるガイドです。安定性の意味、インスタンスの変化、呼び出し側の値のどれがスキップを妨げるかを切り分けます。
主な機能
- 01強スキップ時の比較規則の理解
- 02コンパイラレポートの生成と確認
- 03不安定なパラメーターとインスタンス変化の診断
- 04Lazy アイテム入力の安定化
- 05再測定による修正の検証
活用シーン
- 01UI 操作中の再コンポーズの調査
- 02Composable がスキップされない原因の確認
- 03Lazy アイテムのラムダによる再コンポーズの削減
- 04リリース前の安定性変更の検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ chrisbanes/ skills --skill compose-stability-diagnosticsインストール後、Claude Code で/compose-stability-diagnosticsを入力して読み込まれたか確認します。
詳細
Compose の安定性に関する診断
基本原則
Compose のパラメーターを修正する際は、証拠から始めます。まずコンパイラのモードとパラメーターの比較動作を特定し、次に実際にスキップを妨げているモデルまたは呼び出し箇所を変更します。
Kotlin 2.0.20 以降では、強いスキップがデフォルトで有効です。不安定なパラメーターがあるからといって、再起動可能なコンポーザブルが自動的にスキップ不可になることはありません。ただし、不安定なパラメーターはインスタンスの同一性(===)で比較され、安定したパラメーターは等価性(equals)で比較されます。インスタンスが頻繁に入れ替わる不安定なパラメーターは、依然としてスキップを妨げることがあります。
診断手順
- 症状を確認します。再コンポジションの回数、コンパイラレポートの出力、または頻繁に入れ替わると疑われるパラメーターを確認します。
- コンパイラのモードを特定します。Kotlin/Compose コンパイラのバージョンと、強いスキップが有効かどうかを確認します。
- リリース対象のバリアントについて、Compose コンパイラレポートを生成するか読み込みます。
- 疑わしい各パラメーターについて、問題が安定性の意味論、インスタンスの頻繁な入れ替わり、または呼び出し元が作成するラムダ/派生値のどれにあるかを判断します。
- 型または呼び出し箇所の実態に合う、最小限の修正を適用します。
- 問題が解決したと判断する前に、同じ操作を再測定するか、同じレポートを読み直します。
1. まず強いスキップを把握する
Kotlin 2.0.20 以降では、強いスキップがデフォルトで有効です。このモードでは次のようになります。
- 再起動可能なコンポーザブルは、明示的に無効化されていない限り、パラメーターが不安定でもスキップ可能です。
- 安定したパラメーターは
equalsで比較されます。 - 不安定なパラメーターはインスタンスの同一性(
===)で比較されます。 - コンポーザブル内のラムダは、キャプチャに基づいて自動的に記憶されます。
次の点を確認してください。「これらのパラメーターは期待どおりに比較されるか、また呼び出し元はフレームごとに新しい不安定なインスタンスを作成していないか?」
古いコンパイラ設定、または強いスキップが無効な場合は、従来のルールも引き続き重要です。不安定なパラメーターを持つ再起動可能なコンポーザブルは、再起動可能でもスキップ不可の場合があります。
2. コンパイラレポートを生成する
Kotlin 2.0 以降では、Compose Compiler は Kotlin Gradle プラグインを通じて設定します。
plugins {
alias(libs.plugins.android.application) // or android.library / jvm
alias(libs.plugins.kotlin.android) // or kotlin.multiplatform / kotlin.jvm
alias(libs.plugins.compose.compiler)
}
if (providers.gradleProperty("composeReports").orNull == "true") {
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
metricsDestination = layout.buildDirectory.dir("compose_compiler")
}
}
次に、設定を確認したいバリアントをビルドします。例:
./gradlew :app:assembleRelease -PcomposeReports=true
実行時のプロファイリングには、リリースビルドまたはデバッグ不可能なビルドを使用します。コンパイラレポートはビルド時の出力なので、重要なのは、対象のバリアントとリリースするコンパイラフラグが一致していることです。
主なファイル:
| ファイル | 内容 |
|---|---|
<module>-classes.txt | クラスとプロパティの安定性 |
<module>-composables.txt | 再起動可能/スキップ可能の状態とパラメーターの安定性 |
<module>-composables.csv | 同じデータを並べ替え可能な形式で記録したもの |
<module>-module.json | 集計メトリクス |
3. 問題が実証されたパラメーターだけを修正する
型の不変性または等価性の意味論を正しく表す、最小限の修正を選びます。
不変コレクション
レポートで UI 状態にコレクションインターフェースが使われていることがわかった場合は、UI 状態の境界で kotlinx.collections.immutable を優先します。
// Before: unstable collection interfaces
data class UiState(val items: List<Item>, val tags: Set<String>)
// After: immutable collection contracts
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
data class UiState(val items: ImmutableList<Item>, val tags: ImmutableSet<String>)
生成側では、境界で .toImmutableList()/.toImmutableSet() を一度だけ呼び出して変換します。
@Immutable/@Stable
- すべてのプロパティが実質的に不変で、等価性が観測可能な状態をすべて表す場合は、
@Immutableを使用します。 - Compose から可変状態を観測できる型(通常は
MutableStateを介する型)には、@Stableを使用します。
レポートを黙らせるためにアノテーションを付けないでください。誤った安定性の宣言は、UI の更新漏れを引き起こすことがあります。
サードパーティ製の不変型
アノテーションを付けられなくても、正当に不変として扱える型には stabilityConfigurationFiles を使用します。
composeCompiler {
stabilityConfigurationFiles.add(
rootProject.layout.projectDirectory.file("compose_stability.conf"),
)
}
java.math.BigDecimal
java.math.BigInteger
java.time.*
kotlinx.datetime.*
不変であると保証できる型だけを指定してください。java.util.Date のような可変型は指定しないでください。
4. 遅延アイテムの入力を安定させる
呼び出し元の変更によって遅延アイテムの再構成が発生する場合は、モデルにむやみにアノテーションを付けるのではなく、各アイテムに渡す値を安定させます。
アイテムの存続期間中に安定している入力は、外に持ち上げてアイテムごとに記憶します。
// ❌ BAD — new lambda instances when parent recomposes
items(list, key = { it.id }) { item ->
RowCard(
onClick = { onItemClick(item.id) },
isHighlighted = { item.id == selectedId },
)
}
// ✅ GOOD — stable captures for this item instance
items(list, key = { it.id }) { item ->
val onClick = remember(item.id) { { onItemClick(item.id) } }
val isHighlighted = remember(item.id, selectedId) { item.id == selectedId }
RowCard(onClick = onClick, isHighlighted = isHighlighted)
}
行の位置に関するメタデータ(isFirst、isLast、角の半径)も、インデックスだけに依存する値であれば、remember(index) { … } で外に持ち上げます。ただし、これだけで書き戻しや行をまたぐ計測の問題が解決するとは限りません。
外に持ち上げた後、再構成回数のアサーションを使って、フォーカスの移動や項目の挿入を検証します。
クイックリファレンス
| 症状 | 診断 | 修正 |
|---|---|---|
| Kotlin 2.0.20+ を使用しているのに、古いドキュメントでは不安定な型はスキップ不可と記載されている | 強いスキップによってデフォルトの動作が変更されています | 比較の意味とインスタンスの入れ替わりを確認します |
unstable val items: List<Item> | インターフェース型のコレクションです | ImmutableList<Item> または別の真に不変なラッパーを使用します |
unstable val price: BigDecimal | 外部の不変型です | 安定性設定に追加します |
内部に可変状態を持つ型に @Immutable を付けている | 誤った宣言です | モデルを修正するか、アノテーションを削除します |
| 強いスキップを有効にしているのに、Composable が適切にスキップされない | 再構成ごとに新しい不安定なインスタンスが作成されています | 記憶する、外に持ち上げる、または型を安定させる/等価性ベースにします |
| データに変化がないのに、親の再構成で遅延アイテムが再構成される | 親の再構成ごとに新しいラムダまたは派生値のインスタンスが作成されています(§4) | remember(item.id) { … } でアイテムごとに外に持ち上げます |
| レポートが生成されない | Compose コンパイラープラグインがないか、フラグが設定されていません | org.jetbrains.kotlin.plugin.compose を適用し、出力先を有効にします |
適用しない場合
- 問題がフェーズ間の書き戻しや、行をまたぐ計測値の読み取りである場合は、
compose-state-deferred-readsを使用します。 - 問題が、スクロールやアニメーションなど、コンポジション中に読み取られる高速に変化する
Stateである場合は、compose-state-deferred-readsを使用します。 - 再構成回数が実際のデータ変更に見合っている場合。
- バグの原因が余分な処理ではなく、誤ったデータや古い状態である場合。
- テスト専用コードで、レポートをすっきりさせることより読みやすさが重要な場合。
関連項目
compose-state-deferred-reads- フレームレートで変化する状態は、コンポジションではなくレイアウト/描画で読み取るのが適切な場合があります。compose-recomposition-performance- どの再構成の要因が関係しているかわからない場合の入口です。