Compose is fast by default, but poorly structured UIs can recompose too often, causing dropped frames. The key tool is understanding Compose's skipping mechanism — when Compose decides a composable doesn't need to recompose at all.
Skippable Composables
Compose can skip recomposing a function if:
- It was called with the same parameters as last time
- All parameter types are stable (primitive,
@Stable,@Immutable, or known stable types)
// ✅ Skippable: String is stable — Compose skips if title doesn't change
@Composable
fun ArticleTitle(title: String) {
Text(title)
}
// ❌ Not skippable: List<Article> is unstable by default (mutable collection)
@Composable
fun ArticleList(articles: List<Article>) {
// Recomposes whenever the parent recomposes, even if articles is the same
}
// ✅ Skippable: make it stable
@Immutable
data class ArticleList(val items: ImmutableList<Article>)
@Composable
fun ArticleList(articles: ArticleList) {
// Compose can skip this if articles didn't change
}
Compose Compiler Metrics
Enable metrics to see which composables are skippable/restartable:
// build.gradle.kts
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions.freeCompilerArgs.addAll(
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${project.buildDir}/compose_metrics",
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.buildDir}/compose_reports"
)
}
After building, check build/compose_reports/ for files like:
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun ArticleTitle(
stable title: String
)
restartable fun ArticleList(
unstable articles: List<Article>
) // ← "unstable" means it can't skip!
Kotlinx Immutable Collections
Replace List with ImmutableList to make collection parameters stable:
// implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7")
@Composable
fun ArticleList(articles: ImmutableList<Article>) {
LazyColumn {
items(articles, key = { it.id }) { article ->
ArticleItem(article)
}
}
}
// In ViewModel — convert to ImmutableList before emitting:
val articles: StateFlow<ImmutableList<Article>> = repository.articles
.map { it.toImmutableList() }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentListOf())
Lambda Stability
Lambdas are a common source of instability:
// ❌ New lambda instance on every recomposition of parent
@Composable
fun Parent(id: String) {
Child(onClick = { navigateTo(id) }) // new lambda every recompose
}
// ✅ Stable lambda reference
@Composable
fun Parent(id: String) {
val onClick = remember(id) { { navigateTo(id) } } // stable for the same id
Child(onClick = onClick)
}
Deferred Reading: Move State Read Closer to Use
Reading state in a modifier lambda instead of in the composable body defers the read:
// ❌ Reads scrollOffset in ArticleScreen body — recomposes ArticleScreen on every scroll
@Composable
fun ArticleScreen() {
val scrollState = rememberScrollState()
val headerAlpha = scrollState.value / 300f // read here → recompose
Header(alpha = headerAlpha)
Content(scrollState)
}
// ✅ Read inside the modifier lambda — only the modifier reruns on scroll, not the whole composable
@Composable
fun ArticleScreen() {
val scrollState = rememberScrollState()
Box {
Header(
modifier = Modifier.graphicsLayer {
alpha = scrollState.value / 300f // read inside graphicsLayer lambda
}
)
Content(scrollState)
}
}
Strong Skipping Mode
In Compose 1.6+, strong skipping mode makes lambdas stable by default:
// build.gradle.kts
compilerOptions.freeCompilerArgs.add(
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true"
)
With strong skipping, lambdas are compared by identity (not reference equality), making them stable by default. This removes most lambda stability concerns.
Key Takeaways
| Technique | Impact |
|---|---|
@Stable / @Immutable on data classes | Makes composables skippable when parameters don't change |
ImmutableList / ImmutableMap | Stable collection types; prevents spurious recomposition |
remember(key) for lambdas | Stable lambda references across recompositions |
| Deferred state reading in lambdas | Moves recomposition scope down to just the modifier |
| Compose compiler metrics | Shows which composables are skippable vs. always-recomposing |
| Strong skipping mode | Eliminates most lambda instability; enable in Compose 1.6+ |