Jetpack Compose Performance: Diagnosing and Fixing Cross-Module Stability Issues
How inter-module data flows impact Compose performance and practical mitigation strategies
1The Hidden Cross-Module Performance Pitfall
Jetpack Compose screens that work flawlessly in single-module tests often suffer performance degradation when integrated into the main multi-module app. Profiling tools reveal that simple operations like list scrolling trigger full recomposition of all components. This stems from standard Kotlin collections being marked as 'unstable' by the Compose compiler when crossing module boundaries, disabling smart recomposition skipping.
2Compose's Stability Classification System
The Compose compiler categorizes parameter types into three buckets: Immutable (String, Int), Stable (MutableState), and Unstable. Kotlin's standard List<T> interface is considered unstable by default since it might be backed by a mutable ArrayList. This becomes particularly critical for data models crossing module boundaries, causing Compose to lose its intelligent recomposition capabilities.
3The Module Boundary Compiler Penalty
Data classes compiled in pure Kotlin modules (without Compose compiler plugin) lack stability metadata. When a UserProfile class from :core:model is consumed in :feature:profile-impl, its List<String> property causes the entire class to be marked unstable. This forces recomposition of all related Composables throughout the app, even when data hasn't changed.
4Common Mitigation Attempts and Their Drawbacks
Teams typically try three approaches: 1) Annotating every DTO with @Immutable (creates maintenance overhead), 2) Migrating to kotlinx.collections.immutable (requires massive codebase changes), 3) Creating custom stable wrappers (adds boilerplate noise). None prove practical for large-scale production applications.
5Global Stability Configurations
Modern Compose compiler releases and Kotlin 2.0+ introduce Stability Configuration Files. This feature allows centralized declaration of type stability across the project. For instance, you can declare standard collections as stable globally, solving inter-module performance issues without invasive code changes.
6Strong Skipping Mode
Kotlin 2.0+ and Compose Compiler 1.5.4+ enable Strong Skipping Mode by default. It allows Composables with unstable parameters to be skipped if parameters pass an equals() check. While helpful, this doesn't fully solve cross-module instability issues—it merely mitigates their impact.
7Architect's Note: The Importance of Early Detection
These performance issues manifest as gradual frame drops rather than crashes or ANRs, becoming noticeable only after scaling beyond simple single-module samples. Early profiling and stability analysis in multi-module projects is crucial. Implementing Compose metrics as part of your CI pipeline can catch these issues before they impact users.
8Best Practices for Enterprise Scale
For large-scale Android apps: 1) Prefer immutable data structures at module boundaries, 2) Enable stability metadata in Compose modules, 3) Automate stability analysis in CI/CD pipelines, 4) Define global stability configurations early. These steps significantly reduce long-term performance costs and maintain development velocity.
9Future Outlook
The Compose team is working on solving cross-module stability issues at the Kotlin language level. With Kotlin 2.0 integrating the Compose compiler directly into the Kotlin repository, deeper language integration becomes possible. This signals that future versions may provide more fundamental solutions to inter-module performance challenges.