Mastering Jetpack Compose Performance: The Senior Engineer's Field Guide
Beyond the Basics: Understanding Stability, Snapshots, and Optimization Contracts.
Jetpack Compose has fundamentally changed how we build Android UIs, shifting the burden of state synchronization from the developer to the framework. However, that “magic” relies on a strict contract between your code and the Compose compiler.
When your UI stutters, it’s usually because that contract has been broken. To build high-performance, production-ready apps, you need to look past basic syntax and understand Stability, Snapshot State, and Positional Memoization.
1. The Stability Contract: Why List<T> is a "Liar"
Stability is the compiler’s shorthand for “I can skip this.” If a Composable’s inputs are stable and haven’t changed, Compose skips the work entirely.
The Pitfall: In-place mutation of standard collections. Since List is an interface that could be a mutable ArrayList under the hood, the Compose compiler treats it as stable but cannot guarantee its contents won't change.
If you mutate a plain list in place, the reference remains the same. Compose detects changes based on State reads and reference equality. If the reference doesn’t change, the framework assumes no work is needed, leading to the dreaded “UI not updating” bug.
The Senior Fix:
- Reference Equality: Always treat your state as immutable. Use
list = oldList + newItem. Creating a new reference triggers the invalidation Compose needs. - Explicit Stability: Use
@Stableor@Immutableto manually promise the compiler that your data won't change unexpectedly, especially for models in non-Compose modules. - Compiler Metrics: Enable the Compose Compiler plugin to generate a
classes.txtreport. It is the only way to see if your data classes are actuallyskippingorrestartable.
2. Inside the Snapshot System: Why State<T> Works
To master performance, you must understand the “Brain” of Compose: the Snapshot System.
Compose doesn’t just “watch” your variables; it tracks Reads and Writes within a snapshot.
- Read Tracking: When a Composable function runs, it records which
Stateobjects it reads. - Invalidation: When a
Stateobject is written to, it marks all "scopes" that read it as "invalid." - Recomposition: On the next frame, Compose only re-runs the invalid scopes.
Why ArrayList fails: A standard ArrayList has no mechanism to notify the Snapshot system when an item is added. The Solution: Use mutableStateListOf(). It behaves like a MutableList but hooks into the Snapshot system, ensuring every add or remove correctly invalidates the reading Composables.
3. Heavy Lifting & the Recomposition Loop
A Composable is essentially a function that can be called 60+ times per second. Any logic inside it is “hot” code.
The Pitfall: Performing expensive operations — like sorting a 500-item list — directly in the body. While remember helps, its effectiveness is tied to its keys. If your key changes by equality (equals()) every time the parent recomposes, your "memoized" work is recalculated anyway.
The Senior Fix:
- State Mapping: Perform heavy transformations in the
ViewModelusingFlow.map. - DerivedStateOf: Use this when one state is derived from another high-frequency state (like a
LazyListStatescroll offset) to "buffer" updates so you only recompose when a specific threshold is met.
// ❌ DANGEROUS: Re-sorting every time the parent recomposes
val sortedData = items.sortedBy { it.timestamp }
// ✅ ENGINEERED: Re-calculates ONLY when the 'items' reference changes
val sortedData = remember(items) {
items.sortedBy { it.timestamp }
}4. The Identity Crisis: Lazy List Keys
By default, LazyColumn identifies items by their index. If you insert an item at index 0, every subsequent index shifts.
The Pitfall: Omitting the key parameter. This causes structural churn—the framework destroys and recreates UI nodes instead of moving them. This is the #1 cause of "janky" scrolling in complex lists.
The Senior Fix: Always provide a stable, unique ID (e.g., a Database UUID). This enables Positional Memoization, allowing Compose to skip recomposing rows that have simply changed position in the list.
5. Performance Debugging Checklist
Before you start refactoring, use this checklist to identify the actual bottlenecks:
- Enable Compiler Metrics: Check if your Composables are
skippable. - Layout Inspector: Look for high Recomposition counts vs. Skip counts.
- Check for Unstable Parameters: Are you passing a
Map,List, or a third-party class that isn't annotated? - Lambda Memoization: Ensure your event handlers aren’t capturing unstable objects and breaking skipping.
- Profile Allocations: Use the Android Studio Profiler during a scroll to see if you’re creating thousands of temporary objects (like Date formatters) per frame.
🙋♂️ Frequently Asked Questions (FAQs)
Does hoisting state to the ViewModel cause the whole screen to recompose?
No. Recomposition is scoped. Only the specific Composable that reads the State value is invalidated. If your top-level "Screen" Composable doesn't read the value but just passes it down, only the Leaf node that reads it will recompose.
When should I use rememberSaveable?
Use it for UI state that must survive Process Death or Configuration Changes (like rotation). Examples include search bar text or scroll position. Avoid using it for large datasets, as it must be serialized into a Bundle.
Why is my Composable recomposing even with @Immutable?
Check your parameters. In Compose’s stability inference model, if even one parameter is unstable (like a standard ArrayList), the entire Composable becomes "unskippable."
Proving Performance: Real Metrics
In a senior-level environment, “it feels faster” isn’t enough.
- Recomposition Count: Aim for
0counts on static rows during a scroll. - Frame Time: Use Macrobenchmarks to measure the
FrameTimingMetric. Your goal is to keep the 99th percentile under 16ms (for 60fps) or 8ms (for 120fps).
What is the most stubborn recomposition bug you’ve ever squashed? Let’s talk architecture in the comments. 👇
📘 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