Compose Metrics REVEALED 🔥: Make 80% of Your Composable Functions SKIPPABLE

 



The Hidden Truth About Your Compose UI Performance

You’ve written beautiful Jetpack Compose UIs. They look smooth in preview. But on real devices with real data? Jank.

Why? Compose is restarting composables it could skip. And you have zero visibility into what’s happening.

Until now.

Compose 1.2+ ships with compiler metrics that reveal exactly how Compose sees your code:

"skippableComposables": 64,
"restartableComposables": 76 // 12 functions = performance leaks!

This single JSON file tells you which 12 functions are killing your 60fps.


What Do “Restartable” vs “Skippable” Actually Mean?

Restartable = Recomposition Boundary ✅

restartable fun MyComposable(show: ShowUiModel)
  • Good: Compose can restart just this function when state changes
  • Default behavior: All @Composable functions are restartable
  • Think: “This is a checkpoint where recomposition starts”

Skippable = Performance Rocket Fuel ðŸš€

restartable skippable fun MyComposable(show: ShowUiModel)
  • Elite: Compose can skip the entire function if parameters didn’t change
  • Game-changer: Top-level composables skip = entire UI subtrees skip
  • The goal: Make your hot-path composables restartable + skippable

Real example from Chris Banes’ Tivi app:

❌ restartable fun AirsInfoPanel(unstable show: TiviShow)
✅ restartable skippable fun AirsInfoPanel(stable show: ShowUiModel)

Enable Metrics (2 Minutes Setup)

Add to your root build.gradle:

subprojects {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
if (project.findProperty("enableComposeMetrics") == "true") {
freeCompilerArgs += [
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
"${project.buildDir.absolutePath}/compose_metrics",
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
"${project.buildDir.absolutePath}/compose_metrics"
]
}
}
}
}

Run (release build only!):

./gradlew assembleRelease -Pmyapp:enableComposeMetrics=true --rerun-tasks

Output: app/build/compose_metrics/ contains:

  • module.json ← Overall stats
  • composables.csv ← Filter "Not skippable"
  • composables.txt ← Deep dive

The Most Common Problem: “Unstable” Parameters

Spot this pattern:

restartable fun ShowDetails(unstable show: TiviShow, ...)

Why unstable?

  1. Data class lives in non-Compose module
  2. External library types (ViewModel, date classes)
  3. Lists of stable types (Compose compiler limitation)

Metrics gold nugget:

"knownStableArguments": 890,  // Great!
"knownUnstableArguments": 30, // Fix these!

3 Fixes (From Easiest to Hardest)

1. UI Model Classes (Recommended)

// ❌ Data layer (unstable in Compose metrics)
data class TiviShow(val name: String, val genres: List<Genre>)
// ✅ UI layer (next to your composables)
@Immutable
data class ShowUiModel(
val name: String,
val genres: List<Genre>
)

ViewModel maps:

val uiState = showList.map { show ->
ShowUiModel(
name = show.name,
genres = show.genres
)
}

2. @Stable/@Immutable Annotations

@Immutable  // No mutable properties
@Stable // Mutable but notifies Compose
data class ShowUiModel(...)

3. Parameter-Level @Stable (Escape hatch)

@Composable
fun AirsInfoPanel(
@Stable show: TiviShow, // Force stability
modifier: Modifier = Modifier
)

Advanced: @NonRestartableComposable

For tiny forwarding functions:

@Composable
@NonRestartableComposable // Skip restart machinery
private fun Forwarder(show: ShowUiModel) {
AirsInfoPanel(show)
}

Use when: No state reads, just parameter forwarding.


Gotchas You Must Know

Debug vs Release Builds

Debug: modifier = @dynamic LiveLiterals$...
Release: modifier = @static Companion

Always test release builds — Live Literals fake dynamic params.

@dynamic Default Parameters

// These are OK (theme changes are rare)
backgroundColor: Color = MaterialTheme.colors.primarySurface // @dynamic
// These should be @static
elevation: Dp = AppBarDefaults.TopAppBarElevation // Bug?

Your Action Plan (Do This Today)

1. [ ] Run `./gradlew assembleRelease -PenableComposeMetrics=true`
2. [ ] Open app/build/compose_metrics/module.json
3. [ ] Check: skippableComposables / totalComposables > 80%?
4. [ ] Open composables.csv → Filter "Not skippable"
5. [ ] Fix top 3 functions with UI models + @Immutable
6. [ ] Rebuild → Verify metrics improve
7. [ ] Test on real device with JankStats

Pro tip: Start with your largest screen (most composables).


💬 What’s your skippable %? Drop it in comments + subscribe for more Compose deep dives!


EmailId: vikasacsoni9211@gmail.com

LinkedIn: https://www.linkedin.com/in/vikas-soni-052013160/

Happy Learning ❤️

Comments

Featured Articles

Optimize Jetpack Compose: Performance & Best Practices

From ‘Master’ to ‘Main’: The Meaning Behind Git’s Naming Shift

JIT vs AOT Compilation | Android Runtime

Play Store Uploads with Fastlane Supply - 4

Managing App Versioning and Changelogs- 6

Mastering Android App Performance: Expert Insights