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,
Listis an interface. While it lacks mutation methods, it is not strictly immutable. - Mutable Underpinnings: You can pass a
MutableListwhere aListis 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
.conffile to tell Compose that standardjava.util.Listshould be treated as stable globally.
Comments
Post a Comment