androidengineers.Book a session

Jetpack Compose System Design

Perf Tuning & Skipping

article20 minHard

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:

  1. It was called with the same parameters as last time
  2. 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

TechniqueImpact
@Stable / @Immutable on data classesMakes composables skippable when parameters don't change
ImmutableList / ImmutableMapStable collection types; prevents spurious recomposition
remember(key) for lambdasStable lambda references across recompositions
Deferred state reading in lambdasMoves recomposition scope down to just the modifier
Compose compiler metricsShows which composables are skippable vs. always-recomposing
Strong skipping modeEliminates most lambda instability; enable in Compose 1.6+

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Perf Tuning & Skipping | Android System Design | Android Engineers