Recomposition — その概要、状態変更時のUI再構築

著者: IT Sectr 公開日: 2026-06-27 読了時間: 7 分

Recomposition はJetpack Composeのメカニズムで、データ変更時にユーザーインターフェースの一部を自動的に再構築し、手動でのView更新を不要にします。Composable関数が依存する状態変数が値を変更すると、Composeはその関数のみを再起動し、UIツリーの残り部分はそのままにします。Google Android Developers、2026によると、Recompositionを正しく理解することで、不要な再描画を40〜60%削減できます。

重要ポイント

  • Recomposition — 入力データまたはStateが変更されたときのComposable関数の再起動
  • Skipping — パラメーターが変更されていない関数のスキップ(equalsによる比較)
  • Stability はComposeが関数をスキップできるかどうかを決定 — 安定した型は正しく比較される
  • Smart Recomposition はツリー全体ではなく、最小限の関数セットのみを再起動する
  • 再コンポジション は再描画を保証しない — LayoutとDrawingはフェーズをスキップできる

Jetpack ComposeにおけるRecompositionとは

Recomposition は、新しいパラメーターまたは状態の値で、既にCompositionに参加したComposable関数を再実行することです。再コンポジションの主な目的は、インターフェース全体をゼロから再構築することなく、UIツリーを現在のデータと同期させることです。一度だけ発生するCompositionとは異なり、Recompositionは画面のライフタイム中に何百回もトリガーされる可能性があります。

Recompositionはスマート無効化の原理で動作します:Composeは各Composable関数がどのStateオブジェクトを読み取るかを追跡し、依存関係が変更されたものだけを再起動用にマークします。これは、実行中にすべてのState読み取り操作を記録するスナップショットシステムと、これらの依存関係を特定の関数にマッピングするComposerによって実現されます。

理解することが重要です:再コンポジションは即時の画面再描画を意味しません。Composeは3つのフェーズで動作します:Composition(UI記述の構築)、Layout(サイズと位置の計算)、Drawing(キャンバスへのレンダリング)。再コンポジション後に要素のサイズと位置が変更されていない場合、Layoutフェーズをスキップできます。外観が変更されていない場合 — Drawingがスキップされます。この3フェーズアーキテクチャにより、各UI更新のコストが最小限に抑えられます。

再コンポジションのトリガー:関数再起動の原因

再コンポジションには3つの主要なトリガーがあります。1つ目は、Composable関数の本体内で読み取られたStateオブジェクトの変更です。mutableStateOfまたはderivedStateOfが値を変更すると、前回のコンポジションでこのStateの読み取りを登録したすべての関数が再起動用にマークされます。

2つ目のトリガーは、親関数から呼び出されたときのComposable関数のパラメーター変更です。親関数が新しい値を渡すと(例えば、テキストや数値が変更された)、子関数は内部でStateを読み取らなくても再起動されます。Composeはequalsで新しいパラメーター値と古い値を比較し、等しい場合 — 関数はスキップされる可能性があります。

3つ目のトリガーは、CompositionLocalProviderを介したCompositionLocalの変更です。.currentでCompositionLocalを読み取るすべての関数は、プロバイダーが変更されると再起動されます。このメカニズムはMaterialThemeで使用されています:テーマの切り替え(ライト/ダーク)により、MaterialTheme.colorSchemeを読み取るすべてのコンポーネントの再コンポジションが発生します。

kotlin
@Composable
fun RecompositionDemo() {
    var counter by remember { mutableStateOf(0) }
    var text by remember { mutableStateOf("Hello") }

    Column {
        Text("Counter: $counter")  // recomposition when counter changes
        Text("Message: $text")    // recomposition when text changes

        Button(onClick = { counter++ }) {
            Text("+1")
        }
        Button(onClick = { text = "World" }) {
            Text("Change Text")
        }
    }
}

+1ボタンをクリックするとcounterが変更され、最初のText行とColumn自体のみの再コンポジションが発生します。textを表示する2番目のText行は再起動しません。この分離はスナップショットシステムの結果です:各Composable関数は、読み取ったStateオブジェクトのみを認識します。

再コンポジションの最適化:実践的なテクニック

再コンポジションの最適化は、適切なデータ構造の選択から始まります。可変コレクション(mutableListOf)の代わりに不変コレクション(listOf、mapOf)を使用します。Composeはequalsでパラメーターを比較し、コレクションが変更されてもequalsがtrueを返した場合 — 関数は再起動しません。可変コレクションの場合は、要素レベルで正しい変更追跡を実装するSnapshotStateListを使用します。

2つ目のテクニックは、UIの安定した部分を別のComposable関数に抽出することです。画面の一部が頻繁に変更される状態に依存しない場合、パラメーター付きの別の関数に抽出します。再コンポジションが発生すると、安定した関数は同じパラメーターを受け取り、Composeがそれらを比較して実行をスキップします。これは、一部のパラメーターが変更された大きな関数の一部としてその部分を再起動するよりも効率的です。

3つ目のテクニックは、LazyColumnのキーです。LazyColumn、LazyGrid、およびその他の遅延コンテナのアイテムには常にキーを指定します。キーにより、リストの変更時(追加、削除、並べ替え)にComposeが要素を識別できます。キーがないと、Composeは変更があるたびにリストのすべてのアイテムを再起動し、大きなリストでは顕著なパフォーマンス低下を引き起こします。

kotlin
// Optimized structure: stable parts extracted separately
@Composable
fun OptimizedScreen(items: List<Item>) {
    Column {
        Header()                         // does not depend on items — no recomposition
        Spacer(modifier = Modifier.height(8.dp))
        LazyColumn {
            items(items, key = { it.id }) { item ->
                ItemRow(item = item)   // recomposition only for changed items
            }
        }
    }
}

@Composable
fun Header() {
    Text("Item list", style = MaterialTheme.typography.headlineMedium)
}

@Composable
fun ItemRow(item: Item) {
    Text(item.title)
}

ComposeにおけるSkippingとStability

Skipping は、Composable関数のすべてのパラメーターが変更されていない場合にComposeがその実行をスキップするメカニズムです。スキップが正しく機能するには、パラメーター型が安定(stable)している必要があります。Kotlinコンパイラは以下を安定としてマークします:プリミティブ型(Int、Float、Boolean)、String、ラムダ関数、およびすべてのフィールドが安定かつvalであるクラス。

Stability は、カスタムデータクラスに追加できる@Stableまたは@Immutableアノテーションです。クラスに可変フィールド(var)が含まれている場合、コンパイラはそれを不安定と見なし、Composeはそのようなパラメーターを持つ関数をスキップできません。varを持つクラスの場合、スナップショットシステムを通じて変更通知が送信されることを保証する場合は@Stableを使用します。

安定性は、コンパイラフラグ -P "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=. /reports" を使用して確認できます。すべてのComposable関数とそのパラメーターのリストを安定性とともに示すレポートが生成されます。パラメーターが不安定な場合 — その関数のスキップは不可能で、親の再コンポジションごとに再起動されます。

安定性Skipping
Int, Float, Boolean安定はい
String安定はい
ラムダ安定はい
valフィールドのdata class安定はい
varフィールドのdata class不安定いいえ
List<String>不安定いいえ

注:List<String> はインターフェースであり具象実装ではないため、不安定と見なされます。Kotlin Collections ImmutableライブラリのimmutableListOf()を使用するか、リストを@Stableクラスでラップします。ラムダは常に安定です。なぜなら、そのequalsは参照のみを比較し、呼び出しサイトで新しいラムダが作成されると親関数も再起動するからです。

Android Studioでの再コンポジションの監視

再コンポジションの監視には、Android StudioはCompose Recomposition CountsモードのLayout Inspectorを提供します。このモードでは、各Composable関数が再コンポジションの回数と再起動の理由を表示します。これにより、あまりにも頻繁に再コンポーズされる関数を迅速に見つけ、根本原因(不安定なパラメーターや不要なState依存関係)を特定できます。

追加ツール:Compose Metrics(インストルメンテーションテストによる統計収集)およびRecomposition Timer(各関数の実行時間測定)。Googleはプロファイリング段階でこれらのツールを有効にし、リリースビルドでは無効にすることを推奨しています。これらは再コンポジションごとに最大20%のオーバーヘッドを追加するためです。

再コンポジションを分析する際は、不要な再コンポジションパターンを探します:出力UIが変更されるべきでないにもかかわらず関数が再起動するケース。一般的な原因は、rememberなしのラムダ使用で、毎回新しいラムダオブジェクトが作成され、Composeがパラメーターを変更したと見なすことです。解決策:固定キャプチャでremember { } にラムダをラップします。

kotlin
// Bad: new lambda on every parent recomposition
@Composable
fun Parent() {
    Child(onClick = { doSomething() })  // new lambda every time
}

// Good: remember stabilizes the lambda
@Composable
fun Parent() {
    val onClick = remember { { doSomething() } }
    Child(onClick = onClick)  // same reference
}

よくある質問

再コンポジションは画面の再描画を意味しますか?

いいえ、再コンポジションはCompositionフェーズのみです。その後、LayoutとDrawingが実行されます。再コンポジション後に要素のサイズと位置が変更されていない場合、LayoutとDrawingは完全にスキップされ、GPUリソースを節約できます。

再コンポジションはどのくらいの頻度で発生しますか?

アニメーション中、再コンポジションは毎秒最大120回実行できます(120fps)。通常のインタラクションでは毎秒10〜60回です。各再コンポジションがフレームバジェット(8〜16ms)内に収まることが重要です。そうしないと、アプリケーションが遅延します。

Stateが変更されていないのに、なぜ関数が再コンポーズされるのですか?

理由は親関数からのパラメーター変更です。親が(自身の理由で)再起動し、新しい値を渡します。これを避けるには、パラメーターの安定性を確認し、rememberを使用してラムダと計算値を安定化します。

特定の関数の再コンポジションを無効にできますか?

直接的な無効化はありませんが、readInCompositionによる強制スキップがあります — Stateが関数本体の外で読み取られ、依存関係として登録されません。注意して使用してください:関数は変更に反応しなくなり、古いUIにつながる可能性があります。

CompositionとRecomposition、どちらがより高コストですか?

Composition の方が高コストです。すべてのスロットとツリーノードをゼロから作成するためです。Recompositionは既存のスロットを再利用し、値のみを更新します。実際には、画面のCompositionには2〜10msかかり、単一要素の再コンポジションには0.1〜1msかかります。

まとめ

  • Recomposition — Stateまたはパラメーターが変更されたときのComposable関数の選択的再起動
  • スナップショットシステム がStateに対する関数の依存関係を追跡し、再コンポジションをスケジュール
  • Skipping は安定したパラメーター(@Stableまたは不変)を持つ関数でのみ可能
  • 3つのトリガー:State変更、パラメーター変更、CompositionLocal変更
  • List<T> は不安定と見なされる — 正しいスキップには不変コレクションを使用
  • Layout Inspector が各関数の再コンポジションカウンターを表示
  • 推奨:UIの安定した部分を別の関数に抽出し、ラムダにはrememberを使用

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください