How to Use Android Perfetto Tracing to Identify and Resolve UI Thread Jank in Jetpack Compose Applications

In modern Android development, achieving a consistent 60 or 120 frames per second (FPS) is critical for user retention. However, as Jetpack Compose applications grow in complexity, developers frequently encounter “jank”—dropped frames that cause the UI to stutter during animations or scrolling. Traditional logging and simple profiling tools are often insufficient for diagnosing sub-millisecond thread contention or inefficient recomposition logic. To achieve deep, system-level visibility into UI rendering pipelines, Android engineers must rely on Perfetto, the next-generation tracing and performance analysis platform built directly into the Android Open Source Project (AOSP).

The Mechanics of UI Jank in Jetpack Compose

Unlike the legacy XML View system, Jetpack Compose utilizes a declarative UI paradigm driven by state. When state changes, Compose executes a “recomposition,” re-evaluating the affected composable functions. If a recomposition takes longer than the strictly allocated frame window (approximately 16.6ms for 60Hz or 8.3ms for 120Hz), the Android Choreographer fails to deliver the frame to the SurfaceFlinger in time. This missed deadline is visually perceived by the user as jank.

Common culprits of Compose jank include:

  • Heavy synchronous disk I/O or network calls inadvertently executed on the main UI thread.
  • Unstable data models causing the Compose compiler to unnecessarily recompose massive sections of the UI tree.
  • Excessive object allocation during scrolling, triggering aggressive Garbage Collection (GC) pauses that halt the main thread.

Configuring and Capturing a Perfetto Trace

Perfetto operates by capturing a highly granular, time-series record of CPU scheduling, memory utilization, and custom application trace events. Unlike the deprecated Systrace tool, Perfetto utilizes a highly efficient protocol buffer format capable of recording extended durations without exhausting device memory.

To capture a trace relevant to Jetpack Compose, you must first ensure your application is built in Profileable or Release mode. Debug builds introduce severe performance overhead (due to extensive runtime checks and lack of minification) that invalidate tracing results.

Add the following to your AndroidManifest.xml:

<application
    android:profileable="true"
    ... >

Connect the Android device via ADB and utilize the Perfetto command-line interface to initiate a trace. You must specifically enable the view, gfx, and sched atrace categories to capture rendering and CPU scheduling events:

adb shell perfetto -o /data/misc/perfetto-traces/trace_file.perfetto-trace -t 10s \
sched freq idle am wm gfx view binder_driver hal dalvik camera input res memory

While the trace is recording (the 10-second window defined above), physically interact with the Jetpack Compose UI to trigger the janky behavior.

Analyzing the Trace Data

Once the trace is captured, pull the file to your workstation and open it using the Perfetto web UI (ui.perfetto.dev).

adb pull /data/misc/perfetto-traces/trace_file.perfetto-trace

The Perfetto UI presents a dense, multi-track timeline of the Android operating system. To locate Compose UI jank, navigate to the specific process track for your application application. Look for the UI Thread track and expand the Choreographer#doFrame slices.

If a doFrame slice exceeds the 16.6ms boundary, it will be flagged visually. By drilling down into the specific frame, you can observe the exact Compose phases: Composition, Layout, and Draw.

If the Composition phase is disproportionately long, it indicates that your composables are executing expensive operations or that you have a severe recomposition loop. If the Layout phase is delayed, you may have overly nested custom layouts or deeply complex Modifier chains.

Resolving Compose Bottlenecks

Once Perfetto has isolated the specific composable causing the delay, you can apply targeted optimizations.

If Perfetto reveals that a composable is recomposing continuously without reason, it is likely due to the Compose compiler evaluating a parameter as “Unstable.” You can resolve this by annotating your data classes with @Immutable or @Stable, explicitly guaranteeing to the compiler that the data will not change without triggering a new state emission.

If Perfetto shows severe CPU scheduling contention (e.g., the UI thread is forcibly sleeping while waiting for a background thread lock), you must refactor your architecture to utilize Kotlin Coroutines with the Dispatchers.IO or Dispatchers.Default contexts appropriately, ensuring the main UI thread remains strictly dedicated to rendering.

Get the best tech tips delivered straight to your inbox.

Join thousands of readers mastering Apple, Google, Microsoft, and Linux.