Tracing Recomposition in Production

AndroidJetpack ComposePerformanceProfiling
Share on LinkedIn

Recomposition is Compose working as designed — but recomposition that fires when nothing meaningful changed, or that lands on an expensive composable every frame, is where jank comes from. The hard part in production isn't knowing recomposition exists; it's finding which composables are recomposing too much and whether it actually costs frames. Guessing is worthless here. I've watched teams "optimize" a composable that recomposed twice while the real culprit recomposed 400 times a second off-screen. You need to trace it, and Compose now has decent tools for exactly that.

First, reset the intuition

Recomposition is not inherently bad. A skippable composable that only re-runs when its inputs change is cheap and correct. The problems are specific:

So the goal isn't "eliminate recomposition." It's "make composables skippable, and keep expensive ones off the frequently-recomposing paths." Tracing tells you where you're violating that.

Recomposition counts in Layout Inspector

The fastest first look is the Layout Inspector's recomposition counts. Connect to a running debug build, and it annotates the composable tree with two numbers per node: how many times it recomposed and how many recompositions were skipped.

How to read it:

This is the triage step: it points you at suspects. It doesn't tell you the frame cost — that's the next tool.

Composition tracing for frame-level truth

Layout Inspector tells you how often; composition tracing tells you how long and when, on the actual frame timeline. By default, composables show up as generic work in a system trace. Composition tracing adds named trace sections for individual composable functions, so a captured trace shows "this frame spent 6ms in ProductCard recomposition."

Enable it by adding the runtime tracing dependency:

// build.gradle.kts (debug/benchmark)
implementation("androidx.compose.runtime:runtime-tracing:1.6.0")

Then capture a system trace (Android Studio profiler or Perfetto) while reproducing the jank. In the trace you'll see composable-named sections on the main thread timeline. Now you can answer the question that matters: did this recomposition actually land inside a frame that dropped? A composable recomposing 200 times is fine if each is 0.05ms and off the critical path; it's a bug if one lands mid-scroll costing 8ms.

The workflow I use

Putting the tools in order, because using them out of order wastes time:

  1. Reproduce the jank on a real device (mid-range, release-like build). Emulators lie about performance.
  2. Layout Inspector recomposition counts to find composables recomposing disproportionately to their data. Note the suspects.
  3. Composition tracing + system trace to confirm which suspects actually cost frame time during the janky interaction.
  4. Fix the cause — usually stability, not restructuring:
    • Unstable parameters → make the data class stable (immutable, stable types) or wrap it.
    • Lambdas allocated per-recomposition → hoist or remember them.
    • Expensive work in composition → move it out (remember, derivedStateOf, or off the composition entirely).
  5. Re-measure. Confirm the counts dropped and the frame cost went away. Both, not one.

The fixes themselves are the stability rules from Compose performance and recomposition — this article is about finding the offenders, which is the half people skip.

Measuring in a benchmark, not by feel

For anything you want to track over time, wrap the interaction in a Macrobenchmark capturing FrameTimingMetric. This gives you frame durations (including the ugly P95/P99 tail) for a defined journey — scroll the feed, open a screen — so recomposition regressions show up as numbers in CI rather than as a user complaint months later.

@Test
fun scrollFeed() = benchmarkRule.measureRepeated(
    packageName = "com.example.app",
    metrics = listOf(FrameTimingMetric()),
    iterations = 10,
    startupMode = StartupMode.WARM,
) {
    startActivityAndWait()
    device.findObject(By.res("feed")).fling(Direction.DOWN)
}

Composition tracing tells you why a frame was slow in a single session; a frame-timing benchmark tells you whether it's getting worse over releases. You want both — the diagnostic and the guardrail.

A note on production vs debug

Recomposition counts and full composition tracing rely on debuggable/instrumented builds and add overhead, so you don't leave them on in shipped releases. The "production" part of tracing recomposition means reproducing production-like conditions — real devices, release-optimized code paths where possible, realistic data volumes — not literally profiling end users. For genuine field signal, lean on frame-timing metrics via benchmarks and Play Console's stability/performance data, then reproduce and trace locally.

What I'd take away

Chasing recomposition without tracing is guesswork, and Compose gives you two tools that answer different questions: Layout Inspector recomposition counts show how often each composable recomposes and skips, and composition tracing shows how long and where on the frame timeline. Use counts to find suspects, use tracing to confirm which ones actually cost frames, fix the underlying stability issues, then re-measure both the counts and the frame time. Back it with a frame-timing Macrobenchmark so regressions surface in CI. Recomposition isn't the enemy — unnecessary or expensive recomposition on hot paths is, and now you can prove which is which.

Resources

Frequently asked questions

How do I see which composables are recomposing too much?

Use the Layout Inspector's recomposition counts to see how many times each composable has recomposed and how many recompositions were skipped. A composable recomposing far more than the data behind it changes is a red flag. For frame-level timing, enable composition tracing and inspect the trace in Android Studio or Perfetto to see recomposition work on the timeline.

What is composition tracing in Jetpack Compose?

Composition tracing adds named trace sections for individual composable functions to a system trace, so you can see exactly which composables ran during a frame and how long they took. You enable it by adding the runtime-tracing dependency and capturing a system trace; without it, composables show up as generic work. It's the tool for connecting a janky frame to the specific recomposition that caused it.

Does recomposition always cause performance problems?

No — recomposition is normal and cheap when composables are skippable and only run when their inputs change. Problems arise when composables recompose unnecessarily due to unstable parameters, or when an expensive composable recomposes on every frame. The goal isn't zero recomposition; it's ensuring recomposition happens only when needed and stays off the expensive paths.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →