Measuring Jank in Production with JankStats

AndroidPerformanceJankStatsJetpack
Share on LinkedIn

Jank is invisible in the lab and obvious to users, which is why measuring it in production with JankStats matters more than any local profiling session. JankStats is a small Jetpack library that hooks into the platform's frame-timing signals and reports, on real devices, which frames blew past their deadline — and crucially, lets you attach application state to those slow frames so a dropped frame becomes "the feed scroll janked on a 60Hz mid-tier device" instead of an anonymous statistic. That context is the difference between a number you can act on and a number you ignore.

Why lab benchmarks aren't enough

I run Macrobenchmark in CI and I still get jank reports from the field. The reason is simple: my test devices are a handful of clean Pixels, and my users are on thousands of SKUs with background apps, thermal throttling, and 60/90/120Hz panels. A frame budget is 16.6ms at 60Hz but 8.3ms at 120Hz, so the same rendering work is fine on one device and janky on another. You cannot enumerate that in a lab. JankStats accepts this and measures the population you actually have.

Wiring it up

JankStats attaches per-window and needs the current lifecycle to know when to track. The minimal setup:

class MainActivity : ComponentActivity() {
    private lateinit var jankStats: JankStats

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        jankStats = JankStats.createAndTrack(window) { frameData ->
            if (frameData.isJank) {
                reporter.log(frameData)   // send to your analytics sink
            }
        }
    }

    override fun onResume() {
        super.onResume()
        jankStats.isTrackingEnabled = true
    }

    override fun onPause() {
        super.onPause()
        jankStats.isTrackingEnabled = false
    }
}

The callback fires for every frame. You get a FrameData with the frame's start time, duration, whether it was jank, and any state you've attached. Don't log every frame to your backend — that's a firehose. Sample, or only report isJank frames plus periodic aggregates.

The feature that makes it worth it: state

A bare "1.8% of frames janked" tells you nothing about where to look. JankStats lets you push named state onto a StateHolder tied to a view, and any janky frame carries the state that was active when it rendered:

val metricsState = PerformanceMetricsState.getHolderForHierarchy(view).state

// When the user opens the feed:
metricsState?.putState("screen", "feed")
// While a specific list is scrolling:
metricsState?.putState("scrolling", "feed_list")
// Remove when the interaction ends:
metricsState?.removeState("scrolling")

Now your aggregation can group jank by screen and interaction. In practice this immediately concentrates the problem: I've had reports where 80% of janky frames carried screen=feed, scrolling=feed_list, which turned a vague "the app is janky" into "the feed row does too much work in onBindViewHolder." That's a fix you can scope in an afternoon.

How it detects jank under the hood

Understanding the mechanism keeps you from misreading the data:

Because the deadline derives from the current refresh rate, JankStats correctly holds a 120Hz device to a tighter budget. That's the whole point — it measures relative to what the user's display promised, not a fixed 16ms.

Turning frames into a metric you track

Raw jank events aren't a dashboard. What I actually report:

Metric Why it matters
% janky frames (per screen) Headline health per surface
% severe janky frames (>2 deadlines) Severe jank hurts far more than borderline
Jank rate by device tier / refresh rate Isolates "mid-tier only" problems
Jank rate by app version Catches regressions after a release

Segmenting by device tier and refresh rate is what surfaces the real issues, because a p50 across all devices hides the mid-tier disaster. Track it per app version and a jank regression shows up as a step change right after a rollout — the same regression-detection mindset behind profiling ANRs and jank, just measured continuously on real users.

When JankStats points at something, go deeper

JankStats tells you where and how often, not why. Once it fingers a screen, I reproduce the interaction while capturing a Perfetto trace and read the janky frames' main-thread work directly. The two tools form a loop: JankStats narrows the search space in production, Perfetto explains the mechanism locally, you fix it, and JankStats confirms the field number dropped in the next release.

A few field notes from shipping this:

The reason I put JankStats in every app I own is that it closes the gap between "our benchmarks are green" and "users say it stutters." Frame timing measured on the devices people actually hold, tagged with the screen they were actually on, is the only jank metric that has ever led me straight to a fix.

Resources

Frequently asked questions

What is JankStats and how is it different from Macrobenchmark?

JankStats is a Jetpack library that reports per-frame timing on real user devices in production, flagging frames that took longer than their deadline. Macrobenchmark measures frame timing in a controlled lab run before release. They're complementary: Macrobenchmark catches regressions pre-ship, JankStats tells you what your actual users experience on their actual devices.

How does JankStats know a frame was janky?

It reads frame duration from the platform's frame metrics and compares it against a deadline derived from the display's refresh rate, with a multiplier for headroom. A frame that exceeds that deadline is counted as janky. On API 31+ it uses FrameMetrics directly; on older versions it falls back to OnPreDrawListener timing.

What should I attach as state to frame data?

Attach whatever tells you where the jank happened: the current screen name, the scrolling list identifier, whether an animation or network refresh is in flight. When a frame is janky, that state is included in the FrameData, so your aggregation can say 'the feed scroll on mid-tier devices janks', which is actionable, instead of 'jank exists somewhere'.

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 →