Jank, measured
Jank is not 'a slow app'. It is a specific missed deadline with a specific victim — the user's perception of cause and effect. How to measure it, what causes it, and how to tell which stage is at fault.
A user does not experience "frame 1841 took 19 ms". They experience "the list jerked when I flicked it". Jank is the gap between those two statements, and closing it is mostly a measurement problem: find the frame that missed, find the stage that made it miss, fix that stage.
TL;DR#
- Jank is a frame that misses its present deadline, so the display repeats the previous one.
- The budget is 16.67 ms at 60 Hz, 11.11 ms at 90 Hz, 8.33 ms at 120 Hz; a missed deadline costs at least one refresh period.
- Measure with Perfetto's FrameTimeline for detail,
dumpsys gfxinfofor a quick summary, and JankStats in production. - The four common causes: UI-thread overwork, GPU overwork, allocation/GC, and synchronisation with another thread or process.
- A slow frame is not the same as a janky interaction. What matters is whether the frame was on the critical path of a user gesture.
Mental model#
Think of a drummer in a band.
The drummer has to hit the beat every 16.67 ms. If they miss one, the band does not stop — the previous beat just rings a little longer, and the rhythm feels wrong.
A single missed beat is barely noticeable. A run of missed beats makes the song fall apart. And a missed beat during a dramatic pause is far more noticeable than one in the middle of a loud chorus.
Where the analogy breaks#
A drummer plays continuously. A phone's display always presents something, even when the app is doing nothing — the compositor still composes and the panel still refreshes. Jank is defined relative to change: if a static screen drops a frame, nobody can tell, because the frame would have been identical. That is why measuring jank requires knowing which frames were supposed to be different.
The big picture#
Text description of this diagram
The frame pipeline with the per-stage costs visible. The slow-onDraw toggle pushes the draw stage past the budget, showing how one stage overrunning makes the whole frame miss its vsync. The refresh-rate selector shows that the same workload fits at 60 Hz and fails at 120 Hz, which is why a device can feel smooth in one app and janky in another.
The visualisation is a budget, and jank is what happens when the sum exceeds it. The useful question is which term exceeded it.
Deep dive#
What counts as jank#
There are two related definitions, and they measure different things.
Frame-based: a frame took longer than the refresh interval. Easy to compute, but overcounts: a frame that was going to look identical to the last one does not need to be on time.
Deadline-based: a frame missed the deadline the system assigned it, taking into account when it needed to be ready for the display. This is what FrameTimeline models, and it is closer to what users perceive.1
The practical difference matters when a device runs at a variable refresh rate: at 120 Hz the interval is 8.33 ms, so a frame that would have been fine at 60 Hz is now late. The system may lower the refresh rate when it detects that the app cannot keep up, which is one reason "jank" can appear as a sudden drop to 60 Hz rather than a stutter.
Measuring: the three tools#
Perfetto + FrameTimeline. Record a trace with the gfx and view categories while reproducing the problem. The
FrameTimeline track shows each frame as a bar with a deadline; late frames are marked, and the bar shows which stage
consumed the time. This is the tool for attribution.2
dumpsys gfxinfo. A summary, no trace needed. It reports total frames, janky frames as a percentage, and
percentiles. The framestats variant gives per-frame timestamps for the last 120 frames, which is enough to see
whether the problem is consistent or spiky.3
JankStats. A library that reports frame durations and jank in production, with the UI state attached, so you can tell "janky while scrolling the feed" from "janky while idle".4
Reading gfxinfo#
Total frames rendered: 1841
Janky frames: 63 (3.42%)
50th percentile: 6ms
90th percentile: 11ms
95th percentile: 14ms
99th percentile: 31msThe percentage is the headline, but the percentiles tell the story. A 99th percentile of 31 ms means the worst 1% of frames are roughly two refresh periods — visible stutter, even though the average is fine. A 90th percentile close to the budget means the app is running with no headroom and will jank as soon as anything else competes for the CPU.
The four causes#
1. UI-thread overwork. Measure, layout and record are all on the UI thread. A deep hierarchy, a requestLayout
that invalidates a large subtree, or an expensive onDraw all lengthen this stage. Symptom: the UI-thread DrawFrame
slice is long, and input feels laggy at the same time.
2. GPU overwork. Too much overdraw, large blurs, high-resolution textures. Symptom: the UI thread is fine but the
RenderThread's DrawFrame is long and the GPU track is busy. Input still responds, because the UI thread is free.
3. Allocation and GC. Allocating inside onDraw or in a scroll listener produces garbage, which eventually
triggers a collection. A GC pause on the UI thread blocks everything. Symptom: periodic spikes that correlate with
allocation, and a GC slice on the UI thread in the trace.
4. Synchronisation. Waiting on a lock held by a background thread, a Binder call that blocks, or a
runBlocking on the main thread. Symptom: the UI thread is not busy — it is blocked, and the trace shows a
sleep or a lock wait rather than work.
The interaction question#
A frame that misses while nobody is interacting costs nothing perceptually. A frame that misses during a scroll or a tap is exactly what the user notices, because the finger is moving and the content is supposed to follow it.
This is why frame-rate metrics and interaction metrics are different. An app can render 60 frames per second on average and still feel terrible, if the frames that matter arrive late. Gesture handling that queues work into the next frame but then misses it produces a distinctive "sticky" feel that average FPS will never show.
The input ANR connection#
Jank and input ANRs share a cause. The exists because the system expects an app to consume an input event within a bounded time; when the UI thread is blocked or overloaded, the same condition that causes jank can escalate to an ANR.5
That makes jank a correctness issue, not just a polish issue: a sufficiently long main-thread stall stops being a dropped frame and becomes a dialog the user has to dismiss.
FrameTimeline versus jank percentage#
dumpsys gfxinfo gives you a percentage, which is useful for a dashboard and nearly useless for a diagnosis. A
better number is how late the late frames were, and which frames were late.
FrameTimeline models each frame as having a vsyncId, an intended present time and an actual present time. From
those it derives a jankType — the classification of why the frame was late. The classes that matter most:
- App deadline missed: the app did not finish in time.
- SurfaceFlinger deadline missed: the app delivered, the compositor did not finish.
- Dropped frame: the buffer was not ready when the display needed it.
- Buffer stuffing: the app produced frames faster than the display could show them, so the queue filled.
Those have completely different fixes, and the aggregate percentage hides which one you have.
Why "average FPS" is the wrong metric#
An app that renders 60 frames in one second but does it in two bursts of 30 will report 60 fps and feel terrible. The average says nothing about the distribution, and perception is entirely about the distribution.
The useful summary is the count of frames that missed, plus the worst run of consecutive misses. A single dropped frame is invisible; ten in a row is a visible freeze, regardless of the average.
Choreographer callbacks and the doFrame order#
Within a frame, Choreographer runs its callback queues in a fixed order:
- Input callbacks.
- Animation callbacks.
- Traversal callbacks (measure, layout, draw).
- Commit callbacks.
The order matters because each stage consumes the previous stage's result. An animation that modifies a view's translation must run before the traversal that lays it out; an input handler that changes state must run before the animation that reads it.
An app can register into the animation or traversal queue through Choreographer.postFrameCallback. Doing work in
the wrong queue — or doing work in a callback that runs after the traversal — produces a frame of latency, because
the work lands in the next frame.
Coalescing and batching#
Android may batch multiple ACTION_MOVE events into one dispatch when the app is slow to consume them. From the
app's perspective it receives a single event with historical points, accessible through getHistoricalX and
getHistoricalY.
Batching is a latency/throughput trade: it lets a slow app catch up rather than fall further behind, at the cost of losing the exact timing of each intermediate move. For a drawing app, those intermediate points are the whole signal; for a scrolling list, they are noise.
The interaction metric you cannot get from a frame counter#
What actually matters is whether the content tracked the finger. That is measurable — compare the input event's
timestamp with the frame that shows its effect — but it is not what gfxinfo reports. It requires a trace with both
input and graphics tracks, and it is the metric behind complaints like "it feels sticky" that no frame-rate number
explains.
How it evolved#
| API | Version | What changed |
|---|---|---|
| 1 | 1.0 |
|
| 3 | 1.5 Cupcake |
|
| 9 | 2.3 Gingerbread |
|
| 11 | 3.0 Honeycomb |
|
| 12 | 3.1 Honeycomb |
|
| 16 | 4.1 Jelly Bean |
|
| 20 | 4.4W KitKat (Wear) |
|
| 21 | 5.0 Lollipop |
|
| 24 | 7.0 Nougat |
|
| 26 | 8.0 Oreo |
|
| 28 | 9 Pie |
|
| 29 | 10 |
|
| 30 | 11 |
|
| 31 | 12 |
|
| 33 | 13 Tiramisu |
|
| 36 | 16 Baklava |
|
Under the hood#
- JankTracker.cppthe frame-time tracker behind gfxinfo
- the per-frame deadline model Perfetto visualises
- Choreographer.javawhere the frame clock's callbacks run, and where overruns begin
Curiosity corner#
- 60 fps is not a magic number. It is a legacy of film and early displays; 90 and 120 feel smoother, and the perceptual difference between 90 and 120 is smaller than between 30 and 60.
- A dropped frame at 120 Hz costs half as much time as one at 60 Hz, but there are twice as many chances to drop one. High-refresh displays are both more forgiving and more demanding.
- The "Profile GPU rendering" bars are colour-coded by stage, so you can often tell UI-bound from GPU-bound without a trace.
Try it#
adb shell dumpsys gfxinfo com.example.appExample output
Total frames rendered: 2401
Janky frames: 96 (4.00%)
90th percentile: 12ms
99th percentile: 38msadb shell dumpsys gfxinfo com.example.app framestatsExample output
Flags,FrameTimelineVsyncId,FrameTimelineToken,IntendedVsync,Vsync,OldestInputEvent,NewestInputEvent,HandleInputStart,AnimationStart,PerformTraversalsStart,DrawStart,FrameDeadline,FrameStartTime,FrameIntervaladb shell perfetto -o /data/misc/perfetto-traces/jank -t 8s -a com.example.app gfx viewExample output
Wrote trace to /data/misc/perfetto-traces/jankCheck yourself#
Check yourself
At 120 Hz, how much time does one frame have before it misses the display's deadline?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsFrame pacingsource.android.comaccessed 2026-10-27↩
- docsAndroid tracing with Perfettoperfetto.devaccessed 2026-10-27↩
- docsInspect GPU rendering speeddeveloper.android.comaccessed 2026-10-27↩
- docsการแสดงผลช้า | App qualitydeveloper.android.comaccessed 2026-10-27↩
- docsAnrdeveloper.android.comaccessed 2026-10-27↩
- docsRendering | App qualitydeveloper.android.comaccessed 2026-10-27↩
- docsGraphics architecturesource.android.comaccessed 2026-10-27↩
- docsPerfetto documentationperfetto.devaccessed 2026-10-27↩