Skip to content
All Skills

Tracing Recompositions At Runtime

Use this skill to instrument a Jetpack Compose composable with `@TraceRecomposition` from `skydoves/compose-stability-analyzer` so per-recomposition diffs (which state or parameter changed, what value transition) print to logcat under the `Recomposition` tag. Works in release-with-debug-symbols builds where Android Studio Layout Inspector cannot reach, and feeds the IntelliJ / Android Studio plugin's live recomposition heatmap (green under 10, yellow 10–50, red 50+). Covers the Gradle plugin setup, the `ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)` runtime gate that keeps the instrumentation out of production, and the handoff to debug-time Layout Inspector and CI `stabilityCheck`. Use when the user mentions `@TraceRecomposition`, "trace recomposition", "compose-stability-analyzer", "recomposition logcat", "recomposition heatmap", "release-mode recomposition counts", or needs to confirm a stability fix in a release-like build.

Mobile App Development|v1|Updated 7/14/2026|GitHub source
MCP get_skill({ skillId: "tracing-recompositions-at-runtime-46336855" })

Use this skill with your agent

Create a free account and connect via MCP

Get Started Free
# Tracing Recompositions at Runtime — `@TraceRecomposition`, logcat, and the live heatmap

Layout Inspector counts recompositions and surfaces Argument Change Reasons, but it works **only in debug**, where Live Literals and the interpreted Compose runtime inflate counts. `@TraceRecomposition` from `skydoves/compose-stability-analyzer` instruments a composable at compile time and emits per-recomposition diffs (which state changed, what value transition) to logcat under the `Recomposition` tag. The instrumentation works in any build the developer enables it for — including release-with-debug-symbols — and feeds the IntelliJ / Android Studio plugin's live recomposition heatmap.

This skill is the **release-mode complement** to `../../recomposition/debugging-recompositions/SKILL.md`. Layout Inspector is debug-only, fast to set up, and good for the first triage. `@TraceRecomposition` is the ground-truth confirmation: instrument the suspect composable, ship a release+R8 build of the dev APK, run the user journey, and read the per-recomposition log lines.

## When to use this skill

- A composable recomposes more than expected and Layout Inspector counts are inconclusive (the count differs between debug and release, or the suspect is an inline composable not covered by Layout Inspector).
- A stability or strong-skipping fix needs to be confirmed against a release-equivalent build before merging.
- A developer wants per-state-and-per-parameter change diffs printed inline rather than clicking through the Layout Inspector tree.
- A team wants to baseline a composable's recomposition count for an SLO ("PriceTicker recomposes ≤ once per price update; never per parent tick").
- The user mentions `@TraceRecomposition`, "trace recomposition", "compose-stability-analyzer", "recomposition logcat", "recomposition heatmap", or "release-mode recomposition".

## When NOT to use this skill

- The developer just wants recomposition counts in debug — Layout Inspector is faster to set up. See `../../recomposition/debugging-recompositions/SKILL.md`.
- The build is a **production release** with no diagnosis intent — the instrumentation must be gated off. See the runtime-toggle pattern below.
- The team needs a CI gate that fails on **future** stability regressions — runtime tracing is for diagnosis; gating is `../../stability/enforcing-stability-in-ci/SKILL.md` (`stabilityCheck`).
- The need is per-frame timing or end-to-end user-perceived perf, not recomposition counts — that is `../generating-baseline-profiles/SKILL.md` with `MacrobenchmarkRule` + `FrameTimingMetric`.

## Prerequisites

- The Gradle plugin `com.github.skydoves.compose.stability.analyzer` (latest, v0.7.3+) added to the module that owns the composables to instrument.
- An `Application` subclass declared in the manifest, so `ComposeStabilityAnalyzer.setEnabled(...)` can be called from `onCreate()`.
- Compose Compiler reachable from the same module — `org.jetbrains.kotlin.plugin.compose` applied (Kotlin 2.0+).
- A `BuildConfig` field or feature flag the runtime toggle can read. `BuildConfig.DEBUG` works; a custom `BuildConfig.ENABLE_RECOMPOSITION_TRACE` is preferred for production-style profiling builds.
- For the IntelliJ / Android Studio heatmap: the `compose-stability-analyzer` plugin installed from the JetBrains marketplace (or built locally from the GitHub repo).
- Familiarity with `../../recomposition/debugging-recompositions/SKILL.md` so the developer has already named the suspect composable in debug before reaching for runtime tracing.

## Workflow

### 1. Apply the Gradle plugin

In the module's `build.gradle.kts`:

```kotlin
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("org.jetbrains.kotlin.plugin.compose")
    alias(libs.plugins.compose.stability.analyzer)
}
```

In `gradle/libs.versions.toml`:

```toml
[versions]
composeStabilityAnalyzer = "0.7.3"

[plugins]
compose-stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version.ref = "composeStabilityAnalyzer" }
```

### 2. Configure the analyzer

Same `build.gradle.kts`, alongside the plugins block:

```kotlin
composeStabilityAnalyzer {
    enabled.set(true) // compile-time switch; runtime toggle below gates emission
}
```

`enabled.set(true)` controls whether the compiler weaves in the instrumentation. With `enabled.set(false)` no `@TraceRecomposition` annotations have any effect. Leaving it on across all build types is fine — the runtime toggle is the actual production gate.

### 3. Annotate the composables to trace

```kotlin
import com.skydoves.compose.stability.runtime.TraceRecomposition

@TraceRecomposition(traceStates = true)
@Composable
fun PriceTicker(price: Price) {
    Text(price.formatted)
}
```

`traceStates = true` extends the diff to `mutableStateOf` reads inside the composable body, not just parameters. Start with `true` for first investigation; flip to `false` once the cause is known to keep logs compact.

### 4. Add the runtime toggle in `Application.onCreate()`

```kotlin
import com.skydoves.compose.stability.runtime.ComposeStabilityAnalyzer

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
    }
}
```

Without this call, every annotated composable still emits to logcat — including in release. Gate the toggle behind `BuildConfig.DEBUG` or a custom `BuildConfig.ENABLE_RECOMPOSITION_TRACE` so the production APK is silent.

### 5. Reproduce the symptom and read logcat

```bash
adb logcat -s Recomposition:D
```

Sample output for an animated `PriceTicker` whose price changes from `99.0` to `99.5` (illustrative — exact log shape depends on the analyzer version):

```
D/Recomposition: [Recomposition #1] PriceTicker
D/Recomposition:   ├─ [param] price: Price changed (Price(99.0) → Price(99.5))
D/Recomposition: [Recomposition #2] PriceTicker
D/Recomposition:   ├─ [param] price: Price unchanged (skipped via strong-skipping equals)
```

The number after `#` is a per-instance counter — cumulative across the lifetime of the composable's restart scope. A composable that prints `[Recomposition #50]` while only being on screen for two seconds is the smoking gun.

### 6. Open the live heatmap in Android Studio (optional)

Install the **Compose Stability Analyzer** plugin from JetBrains Marketplace. With the plugin installed and the app running, the editor gutter next to each `@TraceRecomposition`-annotated composable shows a color-coded badge. Indicative thresholds (illustrative — see the plugin's settings panel for the current bands):

- **Green** — fewer than 10 recompositions in the current session.
- **Yellow** — 10 to 50 recompositions.
- **Red** — more than 50 recompositions.

Click the badge to jump to a side panel listing each `[Recomposition #N]` entry with its diff. The panel mirrors logcat but groups by composable instance so the developer can spot which `LazyColumn` row is misbehaving without scrolling logcat.

### 7. Chain back to the upstream fix

Runtime tracing names the composable and the changing parameter. The fix lives elsewhere:

- Param recomposes because the type is unstable → `../../stability/diagnosing-compose-stability/SKILL.md` and `../../stability/stabilizing-compose-types/SKILL.md`.
- Param recomposes because of a captured lambda or `Flow` → `../../recomposition/using-strong-skipping-correctly/SKILL.md` and `../../side-effects/collecting-flows-safely/SKILL.md`.
- State read happened in the wrong phase (Composition vs Layout vs Draw) → `../../recomposition/deferring-state-reads/SKILL.md`.
- Layout Inspector showed an Argument Change Reason status that needed to be acted on → `../../recomposition/debugging-recompositions/SKILL.md`.

Once the fix lands, re-run the trace; the post-fix logcat should show one initial `[Recomposition #1]` and no subsequent entries during the same scenario.

### 8. Gate against future regressions in CI

`@TraceRecomposition` is for diagnosis. Preventing the **next** regression is a CI concern: enable `stabilityCheck` in CI per `../../stability/enforcing-stability-in-ci/SKILL.md`, which fails the build when a previously-skippable composable becomes non-skippable.

## Patterns

### Pattern: annotate the suspect, run a release-with-debug build, read the diff

```kotlin
// RIGHT
@TraceRecomposition(traceStates = true)
@Composable
fun PriceTicker(price: Price) {
    Text(price.formatted)
}
```

Sample logcat output (illustrative — exact log shape depends on the analyzer version; the price changes once per second; the surrounding row recomposes once per parent tick):

```
D/Recomposition: [Recomposition #3] PriceTicker
D/Recomposition:   ├─ [param] price: Price changed (Price(99.0) → Price(99.5))
D/Recomposition: [Recomposition #4] PriceTicker
D/Recomposition:   ├─ [param] price: Price unchanged
D/Recomposition:   ├─ [reason] parent restart scope re-invoked; strong-skipping equals matched
```

The second entry is the desirable shape: parent ticked, the `equals()` guard fired, body skipped.

### Pattern: gate the runtime toggle on a build flag

```kotlin
// WRONG
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        // ComposeStabilityAnalyzer.setEnabled(...) is never called.
        // Every annotated composable emits to logcat in every build, including release.
    }
}
// WRONG because: shipping with tracing enabled adds logcat I/O on every recomposition,
// which adds nontrivial overhead on hot composables (LazyColumn rows in particular)
// and pollutes user-installed-app logs on shared devices.
```

```kotlin
// RIGHT
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
    }
}
```

For a release-with-debug-symbols profiling APK, prefer a dedicated flag over `BuildConfig.DEBUG`:

```kotlin
// RIGHT — explicit profiling flag, decoupled from debug
ComposeStabilityAnalyzer.setEnabled(BuildConfig.ENABLE_RECOMPOSITION_TRACE)
```

### Pattern: do not ship `@TraceRecomposition` annotations in production releases

```kotlin
// WRONG
// Production release with @TraceRecomposition still annotated on hot composables and
// ComposeStabilityAnalyzer.setEnabled(true) hard-coded in Application.
// WRONG because: logcat I/O on every recomposition adds nontrivial overhead on hot
// composables. The instrumentation also captures parameter values into log strings,
// which can leak PII if a composable receives a user model.
```

```kotlin
// RIGHT — annotation present, runtime gate keeps it dormant in release
@TraceRecomposition(traceStates = true)
@Composable
fun PriceTicker(price: Price) { Text(price.formatted) }

// Application:
ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG) // dormant in release
```

### Pattern: dial back `traceStates` once the cause is known

```kotlin
// First investigation — verbose
@TraceRecomposition(traceStates = true)
@Composable
fun Feed(state: FeedState) { /* ... */ }
```

```kotlin
// Cause identified, fix shipped, keep the annotation as a tripwire — quieter
@TraceRecomposition(traceStates = false)
@Composable
fun Feed(state: FeedState) { /* ... */ }
```

`traceStates = true` logs every `mutableStateOf` read transition inside the body. That is gold for first triage and noise once the cause is known. Toggling to `false` keeps the per-recomposition counter (still useful as a tripwire) without the per-state diff.

### Pattern: pair runtime tracing with the CI stability gate

`@TraceRecomposition` finds the regression a developer is chasing **right now**. It does nothing about the regression a teammate ships **next week**. Pair it with `stabilityCheck`:

```text
# RIGHT — both layers in place
- @TraceRecomposition annotates suspect composables; runtime toggle gated on BuildConfig.DEBUG.
- ./gradlew :app:stabilityCheck runs in CI per ../../stability/enforcing-stability-in-ci/SKILL.md.
- The .stability baseline updates only after a deliberate review.
```

```text
# WRONG — diagnosis without prevention
- @TraceRecomposition is the only mechanism in place.
- Next PR introduces an unstable type; nothing in CI catches it; the perf regression
  ships and is found again at runtime weeks later.
```

## Mandatory rules

- **MUST** gate `ComposeStabilityAnalyzer.setEnabled(...)` on a build config field (`BuildConfig.DEBUG` or a dedicated `BuildConfig.ENABLE_RECOMPOSITION_TRACE`) or a feature flag. Never hard-code `setEnabled(true)`.
- **MUST** combine runtime tracing with the `stabilityCheck` CI gate from `../../stability/enforcing-stability-in-ci/SKILL.md`. Runtime tracing is for diagnosis; CI gating prevents the next regression. One without the other is half a workflow.
- **MUST NOT** ship a production release with `@TraceRecomposition` instrumentation enabled. The annotation may remain on composables, but `ComposeStabilityAnalyzer.setEnabled(...)` MUST resolve to `false` in the production build.
- **MUST NOT** treat the `[Recomposition #N]` count as a hard SLO without a context (which scenario? which device? release or debug?). Track the count delta across a fixed scenario instead — "post-fix the price-ticker scenario emits 1 entry vs pre-fix 30".
- **MUST** name both the composable and the changing parameter when reporting a finding ("`PriceTicker` recomposes per parent tick because `price` is reported as Changed, but the `Price` data class is `@Immutable` and `equals()` should match"). "It recomposes a lot" is not a finding.
- **PREFERRED:** start with `traceStates = true` for first investigation (richer logs); set to `false` once the cause is known so the trace becomes a quieter tripwire.
- **PREFERRED:** install the IntelliJ / Android Studio plugin to get the gutter heatmap (illustrative bands: green <10, yellow 10–50, red 50+ — confirm against the plugin's current settings) — it surfaces which composable is the offender without grepping logcat.
- **PREFERRED:** keep `@TraceRecomposition` annotations on a small, deliberate set of composables (the screen's hot composables, the `LazyColumn` row composable). Annotating every composable defeats the signal-to-noise ratio of the heatmap.

## Verification

- [ ] `com.github.skydoves.compose.stability.analyzer` plugin applied to the module that owns the composables to instrument.
- [ ] `composeStabilityAnalyzer { enabled.set(true) }` configured.
- [ ] `Application.onCreate()` calls `ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)` (or another build-flag gate). The call is **not** hard-coded `true`.
- [ ] At least one composable annotated with `@TraceRecomposition(traceStates = true)`.
- [ ] `adb logcat -s Recomposition:D` prints `[Recomposition #N] <ComposableName>` lines while reproducing the scenario.
- [ ] Each emitted line names a parameter or state and reports `changed (oldValue → newValue)` or `unchanged`.
- [ ] In a release build (or with the gate flipped off), `adb logcat -s Recomposition:D` prints nothing — the instrumentation is dormant.
- [ ] CI stability gate (`./gradlew :app:stabilityCheck`) is configured per `../../stability/enforcing-stability-in-ci/SKILL.md` so the next regression is caught before it ships.

## References

- `skydoves/compose-stability-analyzer` (Gradle plugin + runtime + IDE plugin) — https://github.com/skydoves/compose-stability-analyzer
- skydoves, "Optimize App Performance by Mastering Stability in Jetpack Compose" — https://medium.com/proandroiddev/optimize-app-performance-by-mastering-stability-in-jetpack-compose-69f40a8c785d
- skydoves, "6 Jetpack Compose Guidelines to Optimize App Performance" — https://medium.com/proandroiddev/6-jetpack-compose-guidelines-to-optimize-your-app-performance-be18533721f9
- Ben Trengrove, "Jetpack Compose: Debugging recomposition" — https://medium.com/androiddevelopers/jetpack-compose-debugging-recomposition-bfcf4a6f8d37
- Ben Trengrove, "Why you should always test Compose performance in release" — https://medium.com/androiddevelopers/why-should-you-always-test-compose-performance-in-release-4168dd0f2c71
- Compose stability — diagnose — https://developer.android.com/develop/ui/compose/performance/stability/diagnose
- Compose performance overview — https://developer.android.com/develop/ui/compose/performance

For the debug-time entry point with Layout Inspector and Argument Change Reasons, see `../../recomposition/debugging-recompositions/SKILL.md`. For the CI gate that prevents the next stability regression, see `../../stability/enforcing-stability-in-ci/SKILL.md`. For acting on the cause once the offending parameter is named, see `../../stability/diagnosing-compose-stability/SKILL.md`, `../../stability/stabilizing-compose-types/SKILL.md`, `../../recomposition/using-strong-skipping-correctly/SKILL.md`, and `../../recomposition/deferring-state-reads/SKILL.md`. For end-to-end user-perceived perf measurement (frame timing, cold startup), see `../generating-baseline-profiles/SKILL.md`.
#android#jetpack-compose#performance#kotlin#compose#state#preservationandroidgradlejetpack-composecompose-stability-analyzer

Related Skills

More skills in Mobile App Development

Android Design Guidelines

Material Design 3 and Android platform guidelines. Use when building Android apps with Jetpack Compose or XML layouts, implementing Material You, navigation, or accessibility. Triggers on tasks involving Android UI, Compose components, dynamic color, or Material Design compliance.

#work-life#productivityMIT

Android Java Skill

Android Java development with MVVM, ViewBinding, and Espresso testing

#github#externalMIT

Android Kotlin

Android Kotlin development with Coroutines, Jetpack Compose, Hilt, and MockK testing

#claude-bootstrap#bootstrapMIT

Appium Skill

Generates production-grade Appium mobile automation scripts for Android and iOS in Java, Python, or JavaScript. Supports real device and emulator testing locally and on TestMu AI cloud with 100+ real devices. Use when the user asks to automate mobile apps, test on Android/iOS, write Appium tests, or mentions "Appium", "mobile testing", "real device", "app automation". Triggers on: "Appium", "mobile test", "Android test", "iOS test", "real device", "app automation", "UiAutomator", "XCUITest driver", "TestMu", "LambdaTest".

#testing#automationMIT

Apple Appstore Reviewer

Serves as a reviewer of the codebase with instructions on looking for Apple App Store optimizations or rejection reasons.

#github-copilot#appMIT

App Rejection Recovery

When the user's app or update was rejected by Apple App Review or Google Play Review and they need to diagnose why, fix it, and resubmit fast. Use when the user mentions "app rejected", "App Review rejection", "guideline violation", "Apple rejected my app", "Google Play rejected", "Play policy violation", "Resolution Center", "metadata rejection", "binary rejection", "guideline 2.1", "guideline 4.3", "guideline 5.1.1", "Sign in with Apple required", "Apple ID rejection", "Play Store suspension", "appeal", "I need to respond to App Review", or "expedited review". For pre-submission listing health, see aso-audit. For metadata-only fixes, see metadata-optimization.

#work-life#productivityMIT

Explore Other Categories

Skills from other categories with shared topics

Auditing Compose Performance

Use this skill to run an end-to-end Jetpack Compose performance audit when the symptom is broad ("the app feels sluggish", "scroll is rough everywhere", "we're starting a perf sprint", "what should we fix first?"). Orchestrates the four-phase Measure → Diagnose → Fix → Verify loop by sequencing the 25 focused skills (release-mode setup, R8, Baseline Profiles, Compose Compiler reports, stability inference, Layout Inspector, `@TraceRecomposition`, stabilization, strong skipping, phase-deferral, derivedStateOf, lazy layouts, lazy prefetch, Modifier.Node, modifier ordering, flow collection, effects, CI gates, hot-reload) and produces a written audit report with Before/After Macrobenchmark numbers. Use when the developer wants a perf sprint kickoff, a pre-release perf gate, onboarding to a perf-troubled codebase, or a written deliverable. Use when the user mentions "audit", "perf review", "perf sprint", "where do I start", or has no specific symptom yet.

Software Engineering#android#jetpack-compose

Configuring R8 For Compose

Use this skill to configure R8 correctly for a Jetpack Compose application — full mode by default, `proguard-android-optimize.txt`, resource shrinking on, and minimal keep rules because Compose ships consumer ProGuard rules. Covers AGP 8.0+ R8 full mode default, R8's Compose-aware optimizations (lambda grouping, `sourceInformation()` stripping, composable arg constant-folding, `ComposerImpl` devirtualization), legitimate keep needs (`@Serializable`, Hilt entry points, reflective `Saver`s), and the AGP 8.x missing-rule reporter / R8 retrace. Cited gain is roughly 75 percent startup and 60 percent frame-render improvement debug-to-release. Use when setting up a new Compose app, when a PR adds an over-broad keep like `-keep class androidx.compose.** { *; }`, when a release build crashes after enabling minification, when APK size needs reduction, or when first enabling minification.

Software Engineering#android#jetpack-compose

Stabilizing Compose Types

Use this skill to fix unstable Jetpack Compose types once a stability diagnosis has identified them. Covers the three-tier strategy — make the type truly stable with val plus immutable fields, mark with @Immutable or @Stable when the source is owned, and use stabilityConfigurationFiles for third-party or Java types. Explains the compiler-level difference between @Immutable and @Stable (static expression promotion), kotlinx.collections.immutable for List/Set/Map parameters, and the StableHolder wrapper escape hatch. Use when the developer asks how to stabilize a User class, a List parameter, java.time.LocalDateTime, a Flow parameter, or when the compiler report shows unstable params and the developer wants the fix. The diagnostic step lives in a sibling skill.

Software Engineering#android#jetpack-compose