Jetpack Compose Under the Hood: The Magic of @ReadOnlyComposable 💡
Unlocking performance by stripping away Slot Table overhead in high-frequency recomposition paths.
If you’ve ever explored the Jetpack Compose source code — perhaps while poking around MaterialTheme or LocalContext—you might have noticed a specific, low-level annotation: @ReadOnlyComposable.
While @Composable is the bread and butter of Android development, @ReadOnlyComposable is a specialized micro-optimization tool used for high-frequency contextual accessors. Let’s unmask how it works at the compiler level and why breaking its contract is so dangerous.
What Exactly is @ReadOnlyComposable?
In standard Compose, most composable calls introduce a group boundary in the Slot Table. This group allows the runtime to track the call site’s identity, manage state, and handle “skipping” logic.
@ReadOnlyComposable changes this fundamental behavior. It tells the compiler:
“I need to be a Composable to read contextual data (like
CompositionLocal), but I promise I won't emit UI nodes or create my own composition group. I am here only to read."
The Technical Shift: Generated Code Comparison
To understand the performance gain, look at what the Compose Compiler roughly generates for both types (this can vary based on compiler versions and optimizations like Strong Skipping):
Standard @Composable
@Composable
fun StandardThemeRead() {
composer.startRestartGroup(12345) // ⬅️ Overhead: Entry in Slot Table
val color = LocalColors.current
// ... logic ...
composer.endRestartGroup() // ⬅️ Overhead: Closing the group
}@ReadOnlyComposable
@Composable
@ReadOnlyComposable
fun OptimizedThemeRead(): Color {
// ⬇️ No start/end group generated.
// Just a direct read from the composer's context.
return LocalColors.current
}By skipping the generation of a composition group around the function body, the compiler reduces the bookkeeping required during a recomposition pass.
📍 Where Google Uses It Internally
You’ll find this pattern used extensively in the Jetpack Compose framework to reduce overhead in theme-heavy recomposition paths:
MaterialTheme.colorScheme: Accessing your primary/secondary colors.LocalContext.current: Grabbing the Android Context.LocalContentColor.current: Determining the current text/icon tint.- Typography & Spacing: Accessing
MaterialTheme.typographyor custom spacing locals.
❌ When NOT to Use It
Because this removes the function’s “identity” in the Slot Table, you must avoid @ReadOnlyComposable if your function needs to do any of the following:
- Emit UI: Calling
Text(),Button(), or any Layout. - Manage State: Using
remember { ... }(requires a group to store the value). - Side Effects: Using
LaunchedEffectorDisposableEffect. - Animations: Hosting
animateFloatAsStateor other animation APIs. - Complex State Logic: Any logic that may later evolve to require
remember, effects, or UI emission.
⚠ Common Mistake
@Composable
@ReadOnlyComposable
fun UserGreeting(name: String) {
// ❌ COMPILE ERROR:
// Standard composables cannot be called inside a ReadOnly context.
Text("Hello, $name")
}The compiler enforces this contract strictly. If you attempt to call a non-read-only composable inside a read-only one, your build will fail.
The Cumulative Gain: Why It Matters
The performance boost of a single call is microscopic. However, in a complex UI tree, theme values are often accessed hundreds of times across a single recomposition pass.
The Analogy: Think of it like a security checkpoint. A standard @Composable requires you to check your backpack at the door (creating a group); a @ReadOnlyComposable allows you to walk straight in with your hands in your pockets because the system knows you aren't bringing anything in or taking anything out.
🙋♂️ Frequently Asked Questions (FAQs)
Is @ReadOnlyComposable faster than @Composable?
Technically, yes, because it bypasses group generation and slot table entry/exit logic. However, this is a micro-optimization intended for simple data “getters.”
Does @ReadOnlyComposable skip recomposition?
No. If the CompositionLocal value you are reading changes, any function calling your read-only composable will still recompose to reflect that new value. It simply participates in that pass with less administrative overhead.
Can I use remember inside @ReadOnlyComposable?
No. remember requires a group in the slot table to "persist" the value across recompositions. Since @ReadOnlyComposable skips group generation, there is nowhere to store the remembered value.
Is @ReadOnlyComposable safe for State reads?
While you can read State<T>, it is generally discouraged for anything other than composer-backed contextual reads (like CompositionLocals). Standard State reads benefit from the skipping and tracking logic provided by standard @Composable groups.
[!CAUTION] The Golden Rule:
@ReadOnlyComposableis a compiler contract, not a hint. Breaking this contract (by attempting to bypass compiler checks) leads to undefined runtime behavior.
What’s your take?
- Do you have high-frequency theme accessors that could be optimized?
- Have you ever seen a performance bottleneck caused by too many nested groups?
- What other “internal” Compose annotations should we deep-dive into next?
Drop a comment below and let’s talk internals!
📘 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