Mobile Development 12 min read

How to Diagnose and Fix Jetpack Compose Performance Pitfalls

Learn how to identify and resolve performance issues in Jetpack Compose by using Layout Inspector, Stability Reports, and configuration files, understanding stable vs unstable parameters, applying strong skipping, and leveraging annotations and wrapper classes to achieve efficient UI recomposition.

AndroidPub
AndroidPub
AndroidPub
How to Diagnose and Fix Jetpack Compose Performance Pitfalls

In Android development, Jetpack Compose offers a concise API and elegant design for building UI, but hidden performance traps can arise when components are recomposed excessively.

1. The "magic" of Compose and performance pitfalls

Compose simplifies complex UI construction, yet over‑recomposition—such as a screen with three text fields causing all components to recompose on each input—can degrade performance. Developers should monitor real issues instead of premature optimization.

2. Performance diagnosis: tools

2.1 Layout Inspector: visualizing recomposition counts

Layout Inspector shows how many times each composable is recomposed; only interactive components should recompose under normal conditions.

Usage note: If the recomposition counter is missing, ensure the Compose version file is not excluded in build.gradle.

packagingOptions {
    // Wrong: excludes the version file, Layout Inspector cannot show counts
    exclude "META-INF/compose_compiler_version.txt"
}

// Correct: keep the version file
packagingOptions {
    pickFirst "META-INF/compose_compiler_version.txt"
}

2.2 Stability Reports: analyzing component stability

Beyond recomposition counts, Stability Reports indicate whether a composable's parameters are considered stable, which drives Compose's "smart recomposition".

Generate visual reports: Apply the compose-compiler-report-html plugin in build.gradle and run ./gradlew assembleDebug composeCompilerReport to produce an HTML report.

plugins {
    id "io.github.zach-klippenstein.compose-compiler-report-html" version "1.0.0"
}

The report highlights unstable parameters, e.g., InputScreenState.components marked red, revealing why unnecessary recompositions occur.

3. Understanding Compose's smart recomposition and stability rules

3.1 Core concept: stable vs unstable parameters

Stable parameters trigger recomposition only when their equals returns false—typical examples are primitive types (Int, String) or data classes with all val properties.

Unstable parameters cause their parent composable to recompose regardless of changes—examples include classes with var properties or unannotated custom classes.

3.2 Strong Skipping (enabled by default in Compose 1.0.20+)

Strong Skipping optimizes the handling of unstable parameters, removing the need to manually cache lambdas with remember.

// Before strong skipping: cache lambda manually
@Composable
fun OldButton(onClick: () -> Unit) {
    val cachedOnClick = remember { onClick }
    Button(onClick = cachedOnClick) { Text("Click") }
}

// After strong skipping: no manual caching needed
@Composable
fun NewButton(onClick: () -> Unit) {
    Button(onClick = onClick) { Text("Click") } // No extra recomposition
}

3.3 How Compose infers stability

Immutable types : primitive types, data classes with only val properties (including nested val fields).

Mutable types : classes containing var, or collections like List which are mutable by default in Java.

4. Practical optimization: four tools to solve stability issues

4.1 Upgrade toolchain: enable strong skipping

Update the Compose compiler and Kotlin version to 1.0.20+ in build.gradle:

dependencies {
    // Compose BOM
    implementation platform("androidx.compose:compose-bom:2023.10.01")
    kapt "androidx.compose.compiler:compiler:1.5.3"
}

plugins {
    id "org.jetbrains.kotlin.android" version "1.8.22"
}

4.2 Global stability configuration file

Create compose-stability-config.txt under src/main and list types to treat as stable:

// Mark all classes in com.example.models as stable
com.example.models.** -> Stable
// Mark Kotlin List as stable (project uses immutable flow)
kotlin.collections.List -> Stable
// Mark ViewModel classes as stable
androidx.lifecycle.ViewModel -> Stable

Reference the file in the module's build.gradle:

android {
    buildFeatures { compose true }
    composeOptions {
        kotlinCompilerExtensionVersion "1.5.3"
        stabilityConfigurationFile file("src/main/compose-stability-config.txt")
    }
}

4.3 Manual annotations

When configuration files are insufficient, annotate classes with @Immutable or @Stable:

// Immutable data class
@Immutable
data class TextInputModel(val id: String, val value: String)

// Stable ViewModel
@Stable
class InputViewModel : ViewModel() {
    private val _state = mutableStateOf(InputScreenState(emptyList()))
    val state: State<InputScreenState> = _state
}

4.4 Wrapper classes for special cases

For types like ByteArray that lack proper equals, wrap them in a data class and implement equality manually:

// Wrapper with custom equals and hashCode
data class GrpcResponseWrapper(val data: ByteArray) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false
        other as GrpcResponseWrapper
        return data.contentEquals(other.data)
    }
    override fun hashCode(): Int = data.contentHashCode()
}

5. Validation and team collaboration

5.1 Effect verification

Layout Inspector : After optimization, only the interacting TextField should show increased recomposition counts.

Stability Report : Parameters like InputScreenState.components and UIComponent.TextInput should appear green (stable), indicating no redundant recompositions.

5.2 Team guidelines

Code review : Ensure data models follow the "all val " rule; prohibit var in designated packages.

Documentation : Record stability configuration rules in project docs to guide new contributors.

6. Summary: three‑step Compose performance optimization

Measure : Use Layout Inspector and Stability Reports to locate unstable parameters.

Optimize : Apply global stability config, enable strong skipping, and use wrapper classes where needed.

Validate : Re‑run tools to confirm reduced recompositions and enforce team standards.

Performance optimizationAndroidstabilityJetpack ComposeRecomposition
AndroidPub
Written by

AndroidPub

Senior Android Developer & Interviewer, regularly sharing original tech articles, learning resources, and practical interview guides. Welcome to follow and contribute!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.