DEV Community

Cover image for πŸš€ Jetpack Compose Performance Audit
ViO Tech
ViO Tech

Posted on

πŸš€ Jetpack Compose Performance Audit

A practical, production-ready guide to auditing Jetpack Compose performance β€” from checklists, automated tests for excessive recompositions, real screen analysis, to a clear comparison with View-based performance traps.


🎯 Why this article?

Jetpack Compose is declarative, but performance problems don’t disappear β€” they just change shape.

In real production apps, most Compose performance issues come from:

  • Excessive recompositions
  • Wrong state boundaries
  • Unstable objects & lambdas
  • Animations running in the wrong phase

This article gives you:

  • βœ… A Compose performance audit checklist
  • βœ… A real automated test to catch excessive recompositions
  • βœ… A step-by-step analysis of a Compose screen
  • βœ… A Compose vs View performance comparison with concrete examples & tests

1️⃣ Performance Audit Checklist for Jetpack Compose

Use this checklist whenever you review a Compose screen or PR.


🧠 A. Composition & Recomposition

βœ” Are states read at the lowest possible composable?

❌ Anti-pattern:

@Composable
fun Screen(vm: VM) {
    val state by vm.state.collectAsState()
    List(state.items)
}
Enter fullscreen mode Exit fullscreen mode

βœ” Better:

items(state.items, key = { it.id }) {
    Item(it)
}
Enter fullscreen mode Exit fullscreen mode

βœ” Checklist:

  • No frequently-changing state read at screen root
  • Use derivedStateOf for computed values
  • Composables recompose only when their inputs change

🧠 B. Object Allocation

βœ” Are objects created inside composition?

❌ Anti-pattern:

Brush.linearGradient(colors)
Enter fullscreen mode Exit fullscreen mode

βœ” Fix:

val brush = remember { Brush.linearGradient(colors) }
Enter fullscreen mode Exit fullscreen mode

βœ” Checklist:

  • Brush, Shape, TextStyle, Path, Outline are remembered
  • Prefer drawBehind for purely visual effects

🧠 C. Stability

βœ” Are your models stable?

@Immutable
data class UiItem(
    val id: String,
    val title: String
)
Enter fullscreen mode Exit fullscreen mode

βœ” Checklist:

  • @Immutable / @Stable used where applicable
  • Avoid MutableList, MutableMap in UI models

🧠 D. Lazy Layouts

βœ” Are keys provided?

items(items, key = { it.id }) { ... }
Enter fullscreen mode Exit fullscreen mode

βœ” Checklist:

  • No index-based keys
  • Item-level state isolated

🧠 E. Animations

βœ” Where does the animation run?

Animation type Correct phase
Layout change Composition
Visual only Draw / graphicsLayer

βœ” Checklist:

  • No animate*AsState driving layout unintentionally
  • Use graphicsLayer for translations, alpha, scale

2️⃣ Demo: Automated Test to Catch Excessive Recompositions

🎯 Goal

Turn recomposition behavior into something testable & enforceable.


Step 1: Create a recomposition counter modifier

val RecomposeCountKey = SemanticsPropertyKey<Int>("RecomposeCount")
var SemanticsPropertyReceiver.recomposeCount by RecomposeCountKey

fun Modifier.trackRecompositions(): Modifier = composed {
    var count by remember { mutableStateOf(0) }

    SideEffect { count++ }

    semantics {
        recomposeCount = count
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Use it in your composable

@Composable
fun ProfileCard(user: User) {
    Box(
        modifier = Modifier.trackRecompositions()
    ) {
        Text(user.name)
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Write a Compose UI test

@Test
fun profileCard_shouldNotRecomposeExcessively() {
    composeRule.setContent {
        ProfileCard(User("1", "Vio"))
    }

    composeRule.waitForIdle()

    val count = composeRule
        .onNode(hasAnyAncestor(isRoot()))
        .fetchSemanticsNode()
        .config[RecomposeCountKey]

    assertThat(count).isLessThan(3)
}
Enter fullscreen mode Exit fullscreen mode

βœ” This test fails automatically if someone introduces excessive recompositions later.


3️⃣ Real-world Analysis: A Compose Screen

πŸ“± Example: Recording List Screen

@Composable
fun RecordingScreen(vm: RecordingVM) {
    val state by vm.state.collectAsState()

    LazyColumn {
        items(state.records) {
            RecordingItem(it)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

❌ Problems

  • Entire list recomposes on any state change
  • No item keys
  • RecordingItem recreated unnecessarily

βœ… Refactored Version

@Composable
fun RecordingScreen(vm: RecordingVM) {
    val records by vm.records.collectAsState()

    LazyColumn {
        items(
            items = records,
            key = { it.id }
        ) {
            RecordingItem(it)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

🎯 Result

  • Only changed items recompose
  • Scroll remains smooth
  • Predictable performance

4️⃣ Compose vs View Performance Traps

πŸ”΄ Trap #1: "Compose is slower than Views"

❌ Wrong comparison:

  • Unoptimized Compose vs optimized RecyclerView

βœ” Fair comparison:

  • Stable models
  • Keys
  • Proper state scoping

πŸ”΄ Trap #2: Frequent invalidation

View system

view.invalidate() // redraw only
Enter fullscreen mode Exit fullscreen mode

Compose equivalent

Modifier.drawBehind { }
Enter fullscreen mode Exit fullscreen mode

❌ Bad Compose equivalent:

mutableStateOf(x++) // triggers recomposition
Enter fullscreen mode Exit fullscreen mode

πŸ”΄ Trap #3: Measuring everything again

System Cost
View requestLayout() expensive
Compose Layout skipped if inputs stable

βœ” Compose wins if stability is respected.


5️⃣ How to Test Performance (Compose & View)

πŸ§ͺ Tools

  • Layout Inspector (Recomposition Count)
  • Recomposition Highlighter
  • Macrobenchmark

πŸ“Š Macrobenchmark example

measureRepeated(
    packageName = "com.vio.app",
    metrics = listOf(FrameTimingMetric()),
    iterations = 5
) {
    startActivityAndWait()
}
Enter fullscreen mode Exit fullscreen mode

🧠 Final Mental Model

Compose performance = state boundaries + stability + correct phase (composition vs draw)

If you control those three, Compose is:

  • Faster than Views
  • More predictable
  • Easier to test

βœ… TL;DR Checklist

  • πŸ”² No unnecessary recompositions
  • πŸ”² Stable models & lambdas
  • πŸ”² Keys everywhere in Lazy layouts
  • πŸ”² Animations in draw phase when possible
  • πŸ”² Performance covered by tests

If you want:

  • πŸ” A performance audit template for your app
  • πŸ§ͺ A ready-to-use testing module
  • 🧠 A deep dive into Compose internals (slot table, skipping, groups)

πŸ‘‰ Let me know β€” happy to go deeper πŸš€

Top comments (0)