Compose Performance Profiling: Tools and Techniques

By Engineering Team • Published: 2025-12-13 • Updated: 2025-12-1316 min read

PerformanceProfilingOptimizationDebugging

Why Performance Profiling Matters

Compose is fast by default, but it is easy to introduce performance problems accidentally. Excessive recompositions, heavy computations during composition, and inefficient state reads can all cause jank.

The good news: Android Studio provides excellent tooling for diagnosing Compose performance issues. Let's explore each tool and when to use it.

Layout Inspector: Visualizing Recomposition

Layout Inspector is your first stop for Compose debugging. Enable recomposition highlighting to see which components update:

  • Open Layout Inspector: View > Tool Windows > Layout Inspector
  • Enable "Show Recomposition Counts" in the toolbar
  • Blue highlights show recomposition count; red shows skipped count
  • Components recomposing excessively appear with high blue counts
Note: A well-optimized Compose UI should show mostly "skipped" (red) counts. High blue counts indicate components that recompose too often.

Composition Tracing with System Trace

For deeper analysis, enable Composition Tracing to see exactly what happens during each frame:

// Add to build.gradle.kts
android {
    buildTypes {
        debug {
            // Enable composition tracing
            manifestPlaceholders["enableComposeCompilerReports"] = true
        }
    }
}

// In code, you can add custom trace sections
@Composable
fun ExpensiveList(items: List<Item>) {
    trace("ExpensiveList") {
        LazyColumn {
            items(items, key = { it.id }) { item ->
                trace("ItemRow-${item.id}") {
                    ItemRow(item)
                }
            }
        }
    }
}

Macrobenchmark: Measuring Real Performance

For production-grade performance testing, use Macrobenchmark. It measures startup time, frame timing, and scrolling performance:

@LargeTest
@RunWith(AndroidJUnit4::class)
class ScrollBenchmark {
    @get:Rule
    val benchmarkRule = MacrobenchmarkRule()

    @Test
    fun scrollProductList() {
        benchmarkRule.measureRepeated(
            packageName = "com.example.app",
            metrics = listOf(FrameTimingMetric()),
            iterations = 5,
            startupMode = StartupMode.COLD,
            setupBlock = {
                startActivityAndWait()
            }
        ) {
            // Scroll the product list
            val list = device.findObject(By.res("product_list"))
            list.setGestureMargin(device.displayWidth / 5)
            
            repeat(3) {
                list.fling(Direction.DOWN)
                device.waitForIdle()
            }
        }
    }
}

Common Performance Anti-Patterns

Here are the most frequent causes of Compose performance issues:

// ❌ Anti-pattern 1: Creating objects during composition
@Composable
fun BadExample() {
    val items = listOf(1, 2, 3) // New list every recomposition!
    val onClick = { doSomething() } // New lambda every recomposition!
}

// ✅ Fix: Use remember
@Composable
fun GoodExample() {
    val items = remember { listOf(1, 2, 3) }
    val onClick = remember { { doSomething() } }
}

// ❌ Anti-pattern 2: Reading state too high
@Composable
fun BadScreen(viewModel: MyViewModel) {
    val scrollState = viewModel.scrollState.collectAsState() // Read at top
    Column {
        Header() // Recomposes even though it doesn't use scrollState
        Content(scrollState) // Only this needs scrollState
    }
}

// ✅ Fix: Read state where it's used (defer reads)
@Composable
fun GoodScreen(viewModel: MyViewModel) {
    Column {
        Header() // Won't recompose when scrollState changes
        Content(viewModel) // Read scrollState inside Content
    }
}
Note: Profile before optimizing. Premature optimization often makes code harder to read without meaningful performance gains.
← Back to Blog