Stop Over-Using @Stable: The Performance Tool You're Misunderstanding
Beyond the @Stable annotation: A senior-level guide to mastering recomposition boundaries and compiler inference.
TL;DR
- Don’t annotate everything:
@Stableis a precision tool, not a default "performance button." - Unstable ≠ Automatic Recompose: Unstable types simply prevent the compiler from guaranteeing a skip.
- Use it when: The compiler fails to infer stability (Interfaces, Multi-module) or you have verified a skipping failure.
- The Risk: Applying
@Stableincorrectly creates "stale UI" bugs that are a nightmare to debug.
How Stability Enables Skipping
Compose relies on a mechanism called Skipping. If a Composable’s inputs haven’t changed, Compose attempts to skip its execution. However, if the compiler cannot prove a type is stable, it fails to apply the skipping optimization.
The Mental Model:
- Stable Input +
equals()unchanged → Skip ✅ - Unstable Input → Cannot guarantee → Recompose ❗
The Technical Nuance:
Stability affects parameter comparison, not state observation. Compose still tracks State reads independently; stability only dictates whether the Composable can skip based on its arguments.
Note on Strong Skipping Mode: Modern Compose includes “Strong Skipping Mode,” which allows the compiler to skip more aggressively even with certain unstable parameters, reducing the manual burden on developers.
The @Stable Contract
When you apply @Stable, you are signing a contract the compiler trusts without verification. If you break these rules, your UI will fall out of sync with your data:
- Result Consistency:
equals()must return the same result for the same two instances. - UI Equality:
equals()must accurately reflect whether the UI output should change. - Observable Changes: Any property change must notify Compose (e.g., via
MutableState).
Real-World Implementation: Before & After
Scenario A: The Redundant Annotation
// BEFORE: The compiler ALREADY infers this as stable.
// Adding @Stable here is redundant "code rot."
data class User(val id: Int, val name: String)
@Composable
fun UserHeader(user: User) { ... }Scenario B: The Critical Fix (Interfaces)
// AFTER: Use @Stable here because the compiler cannot "see"
// through the interface to verify the implementation's stability.
@Stable
interface NavigationItem {
val route: String
val icon: ImageVector
}
@Composable
fun NavBar(item: NavigationItem) { ... }🚫 Common Mistake: The “Stable” Lie
A frequent performance “fix” that breaks apps is marking a class as @Stable while it holds a standard collection.
@Stable // DO NOT DO THIS
data class TodoState(
val items: List<String>
)Why it fails: List is treated as unstable because it could be backed by a mutable implementation. If you add an item to the underlying mutable list but the list reference stays the same, equals() passes. Because you promised @Stable, Compose skips the recomposition. Result: Your new Todo item never appears on the screen.
The Fix: Use PersistentList from Kotlinx Immutable Collections or wrap the list in a class marked @Immutable.
How to Detect Unnecessary Recompositions in Jetpack Compose
- Layout Inspector: Look for the Skips column. If a Composable is recomposing while the parent changes but “Skips” remains at 0, your parameters are likely the bottleneck.
- Compose Compiler Reports: The gold standard. It generates a text report during build time labeling every class as
stable,unstable, orruntime. This is the only way to see exactly what the compiler sees.
🙋♂️ Frequently Asked Questions (FAQs)
Is it a bug to have unstable classes?
No. For simple “leaf” Composables, the cost of a defensive recomposition is often negligible.
Does @Stable guarantee my Composable will skip?
No. It enables the possibility. Recomposition can still occur if the scope is invalidated or internal state changes.
Should I use @Stable by default on all UI State?
No. Trust the compiler first. Manual annotations should be the result of a deliberate optimization choice, not a boilerplate habit.
💬 Final Takeaway
If you feel the need to add @Stable everywhere, you're likely fixing a symptom rather than the architecture. Trust the compiler first, measure second, and annotate last.
Discussion:
- Have you caught a “stale UI” bug caused by an incorrect
@Stableannotation? - How has Strong Skipping Mode changed your optimization workflow?
- What’s the most surprising “unstable” culprit you’ve found in a Compiler Report?
📘 Master Your Next Technical Interview
Since Java is the foundation of Android development, mastering DSA is essential. I highly recommend “Mastering Data Structures & Algorithms in Java”. It’s a focused roadmap covering 100+ coding challenges to help you ace your technical rounds.
- E-book (Best Value! 🚀): $1.99 on Google Play
- Kindle Edition: $3.49 on Amazon
- Also available in Paperback & Hardcover.

Comments
Post a Comment