Mastering Compose Performance: Stability, Reports, and Profiling

Performance in Jetpack Compose is built on Stability—the compiler's ability to know if data has changed. If the compiler isn't sure, it re-runs your UI code "just in case," leading to unnecessary recompositions and potential lag.

1. Stable vs. Unstable Types

The Compose compiler categorizes every parameter passed into a function to determine if a Composable can be "skipped."

Type Description Examples
Stable Immutable types or those that notify Compose of changes. Compose skips these if .equals() is true. String, Int, Boolean, @Stable objects, State<T>.
Unstable Types that might change internally. Compose always recomposes these to ensure UI accuracy. Standard List, Set, Map, or classes with var properties.

Why is List<T> considered Unstable?

  • It's an Interface: In Kotlin, List is an interface. While it lacks mutation methods, it is not strictly immutable.
  • Mutable Underpinnings: You can pass a MutableList where a List is expected. If that list is modified elsewhere, the reference stays the same but the data changes.
  • Compiler Caution: Because the compiler cannot guarantee the contents won't change, it treats all standard collections as Unstable.

2. Audit: Using Compiler Reports

To find out exactly why a parameter is unstable, you can generate a Stability Report.

How to generate the report

Add this to your build.gradle.kts:

composeCompiler {
    reportsDestination = layout.buildDirectory.dir("compose_compiler_report")
    metricsDestination = layout.buildDirectory.dir("compose_compiler_metrics")
}

Run ./gradlew assembleRelease. Navigate to your build folder to find classes.txt and composables.txt.

Finding the "Why" (classes.txt)

This file explicitly flags the culprit parameter:

unstable class UserState {
  stable val id: Int
  unstable val tags: List<String> // <--- This parameter makes the whole class unstable!
  <runtime stability> = Unstable
}

Interpreting Function Status (composables.txt)

This file shows if your functions are Skippable (efficient) or just Restartable (forced to re-run):

// Bad: Must run whenever the parent does
restartable fun UserRow(unstable user: UserState)

// Good: Can be bypassed if data hasn't changed
restartable skippable fun UserRow(stable user: UserState)

3. Advanced Profiling: Finding Real-World Lag

Even with stable code, internal logic can cause "jank." Use these tools to profile your UI:

Layout Inspector (Live Debugging)

Open Tools > Layout Inspector and enable "Show Recomposition Counts".

  • Recomposition Count: High numbers during a scroll indicate a "hot" Composable.
  • Skips: If this matches your recompositions, your stability logic is working.

System Tracing (CPU Profiler)

Record a System Trace to see a flame chart of your Composables.

  • Jank Detection: If a frame takes longer than 16.6ms, it will be flagged. You can see exactly which function was executing when the frame dropped.
  • Pro Tip: Always profile in Release Mode. Debug builds include overhead that makes Compose look significantly slower.

4. Modern Solutions

  • Strong Skipping Mode: Defaults in Kotlin 2.0+. It skips even "unstable" types if the instance (reference) hasn't changed.
  • ImmutableList: From kotlinx.collections.immutable. These are strictly immutable and marked Stable.
  • Stability Config: You can create a .conf file to tell Compose that standard java.util.List should be treated as stable globally.
Summary: Use Compiler Reports to find Unstable types, apply Strong Skipping or ImmutableList to make them Stable, and verify with the Layout Inspector.

Comments

Popular posts from this blog

Enhancing LLM Responses with Prompt Stuffing in Spring Boot AI

Automate Library Integration with Cursor's Agent Mode

Using AndroidViewModel for accessing application context