In Jetpack Compose, the declarative UI paradigm simplifies state synchronization, but unintentional recomposition cascades remain one of the primary causes of UI micro-stutters (jank) and battery drain.
While the desktop Android Studio Layout Inspector provides recomposition counts, diagnosing performance regressions on physical devices in real-world conditions (without being tethered to a workstation over USB) has historically been challenging.
To solve this, we built compose-recomposition-highlighter — an open-source, on-device visual performance auditor for Jetpack Compose.
🏗️ Architectural Overview & How It Works
compose-recomposition-highlighter operates through a lightweight modifier and a thread-safe recomposition registry:
-
Draw-Phase Tracking: Uses
Modifier.drawWithCacheto read and increment render passes during the draw phase, ensuring the tracking itself does not trigger additional recomposition loops. -
Dynamic Heatmap Mapping: Maps the render frequency to a high-contrast color gradient:
- 🟢 Green (1–2x): Healthy, expected recomposition.
- 🟡 Yellow (3–5x): Moderate activity.
- 🟠 Orange (6–9x): High recomposition rate.
- 🔴 Red (10x+): Severe hotspot / infinite recomposition alert.
-
Hotspot Velocity Metrics: Calculates velocity (
recomps/sec) to help you spot runaway loops immediately.
🛠️ Step-by-Step Implementation Guide
1. Add Gradle Dependency
Add the library from Maven Central using debugImplementation so it is automatically stripped from release builds:
dependencies {
// Debug builds: full highlighter engine + UI overlay
debugImplementation("io.github.zakayothuku:compose-recomposition-highlighter:1.0.0")
// Release builds: zero-overhead no-op artifact
releaseImplementation("io.github.zakayothuku:compose-recomposition-highlighter-noop:1.0.0")
}
2. Attach the Modifier to Target Composables
Attach .recompositionHighlighter() to any composable you want to monitor:
@Composable
fun UserProfileCard(
user: UserUiModel,
onFollowClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.recompositionHighlighter(tag = "UserProfileCard")
.padding(16.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = user.name, style = MaterialTheme.typography.titleMedium)
Text(text = user.bio, style = MaterialTheme.typography.bodyMedium)
Button(onClick = onFollowClick) {
Text(text = if (user.isFollowing) "Following" else "Follow")
}
}
}
}
3. Mount the Floating Performance HUD
In your root application screen or navigation host, wrap your content with ComposeRecompositionOverlay:
@Composable
fun MainAppScreen() {
Box(modifier = Modifier.fillMaxSize()) {
// App Navigation & Screens
AppNavigationGraph()
// Floating Draggable On-Device Recomposition Monitor
ComposeRecompositionOverlay(
enabled = BuildConfig.DEBUG,
showCountBadges = true,
warningThreshold = 5,
criticalThreshold = 10
)
}
}
🔍 Common Jetpack Compose Traps You Will Catch
-
Unstable Lambda Parameters: Passing non-memoized lambdas (e.g.
{ viewModel.doSomething(item.id) }) insideLazyColumnitems causing whole-list invalidations. -
Un-remembered Derived Computations: Calculating heavy sorted lists or date formats directly inside the composable body without
remember { ... }. - Over-Observing StateFlows: Observing a coarse-grained UI state object when a composable only needs a single primitive boolean.
👉 GitHub Repository: github.com/zakayothuku/compose-recomposition-highlighter

Top comments (0)