Project Butter
Android 4.1's answer to the lag everyone complained about: a vsync clock, triple buffering, and a framework object called Choreographer. Why the fix was structural, not a speed-up.
Before 2012, Android's UI ran as fast as it could rather than as fast as the display. That sounds like an advantage until you watch it: frames finished at arbitrary times, the compositor showed whichever one was newest, and the result was visible stutter even on hardware with plenty of headroom. Project Butter did not make Android faster. It made Android punctual.
TL;DR#
- Project Butter shipped in Android 4.1, API 16, announced at Google I/O 2012.
- It introduced vsync-driven rendering: drawing is scheduled against the display's refresh signal, not run free-form.
- It added triple buffering, so one late frame costs latency instead of stalling the producer.
- It added Choreographer, the framework object that queues animation work for the next frame.
- It is the moment Android stopped treating smoothness as a performance property and started treating it as a scheduling property.
Mental model#
Imagine a bus that leaves every ten minutes.
Before Butter, passengers showed up whenever they were ready and the bus left whenever it had one. Departures were unpredictable, some passengers waited a long time, and two buses sometimes left a minute apart.
After Butter, the bus leaves on a fixed timetable. If you miss it, you wait for the next one — but everyone knows when the next one is, and the trip becomes predictable.
Where the analogy breaks#
A missed bus costs you ten minutes. A missed vsync costs you one frame of latency, and that frame's work does not disappear — it joins the next frame, which now has twice as much to do. The system can fall into a pattern where every frame is late, which is why the timing fix had to come with a buffering fix. Scheduling alone would have created a queue of perpetually late passengers.
The big picture#
Text description of this diagram
The frame pipeline with a vsync clock. Before Project Butter, the app drew whenever it was ready and the compositor showed the latest buffer, so pacing was arbitrary. After it, the UI thread and RenderThread schedule work against the display's vsync, and three buffers absorb jitter. The diagram shows the same eight stages and the same 16.67 ms budget, but with the deadline as the organising principle.
The diagram is the same pipeline every Android frame uses today. That is the point of Butter: it did not add a stage, it changed what the stages are racing against.
Deep dive#
The problem: free-running rendering#
In the pre-Butter model, an app drew when something invalidated it. The compositor woke up, took the newest available buffer, and put it on screen. Two consequences followed.
First, frame pacing was arbitrary. If the app produced frames slightly faster than the display could show them, some were shown and some were skipped, and the cadence was visibly uneven even though the average frame rate was fine. Second, input latency varied. A touch might land just after a draw and wait a whole frame, or just before one and be included immediately.
The complaint at the time was "Android is laggy". The measurement that explained it was not average frame rate but frame-time variance — the same average with less jitter looks dramatically smoother.
The fix, part one: a shared clock#
Butter made the vsync a first-class signal. SurfaceFlinger receives it from the display, timestamps it, and broadcasts it to apps that are drawing.1 An app that wants to draw at 60 Hz asks for a callback one vsync ahead and does its work inside the interval.
The framework object for that request is Choreographer. Its postFrameCallback runs at the beginning of the next
frame with the frame's timestamp, and its internal callback queues impose an order: input, animation, traversal,
commit.2 That ordering is what makes a frame internally consistent — the animation's
output is visible to layout, and layout's output is visible to draw, all within one frame.
The fix, part two: one more buffer#
If a frame misses its deadline, what happens next depends entirely on how many buffers exist.
With two buffers — one being displayed, one being filled — a late frame means the producer has nowhere to write until the display releases its buffer. It stalls, then starts late, then is late again. The system locks into the pattern the analogy warned about.
With three, the producer can be filling one buffer while the compositor holds the other two. A single late frame is absorbed: it is shown one frame later, but the producer was never blocked, so the next frame starts on time.
3Text description of this diagram
A vsync timeline at 60 Hz with the same wobbly workload, first with two buffers and then with three. With two buffers, a 21 ms frame leaves the producer with no free slot, so it stalls and the next frame is late too. With three buffers, the same frame is absorbed and the producer keeps working, at the cost of up to one extra frame of latency.
The cost is latency. With three buffers, the buffer you are drawing into may not be shown for up to an extra frame, because a newer one can overtake it. Butter chose smoothness over that extra frame of latency, and that trade is still the default.
The fix, part three: a place to put animation#
Choreographer gave animation a home. Before it, animations were driven by whatever posted messages to the UI thread, so their timing was tied to the message queue rather than the display. After it, an animation registers a frame callback and gets a timestamp that it can use to compute exactly how far along it should be:
val animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 300
addUpdateListener { update(it.animatedValue as Float) }
}
animator.start()ValueAnimator uses the frame clock internally, so the same duration produces the same visual speed on a 60 Hz and a
120 Hz display: more frames, not faster animation. That distinction — time-based, not frame-count-based — is one of
Butter's durable legacies.
What Butter did not fix#
Butter aligned the clock and widened the pipe. It did not make drawing itself cheaper, and it did not move GPU work
off the UI thread — both remained true for another two years. onDraw still ran on the thread that handled input, so a
slow view still blocked touches. The RenderThread arrived in API 21Android 5.0.
Nor did it fix composition. If an app's layer could not be scanned out directly, SurfaceFlinger still had to compose it, and on the hardware of the era that could be the most expensive step in the frame.
Why the buffering change was the controversial part#
The vsync clock was uncontroversial. Triple buffering was not: it trades latency for smoothness, and the extra frame of latency is felt most in exactly the interactions where it matters — typing, drawing, dragging.
With two buffers, the buffer being drawn is shown at the very next vsync after it is queued. With three, a newer buffer can overtake the one you just finished, and the one you drew can wait an extra refresh period. At 60 Hz, that is up to 16.67 ms of additional input-to-photon delay.
The trade was made deliberately, and it is still visible in the platform's behaviour: input latency benchmarks on Android are consistently a frame or two worse than the theoretical minimum, and that is the price of not stalling.
Modern adaptive refresh rates complicate it further. At 120 Hz the extra frame costs half as much time, so the latency penalty of triple buffering is smaller — one reason high-refresh devices feel both smoother and more responsive.
Choreographer's ordering, and why it is still the same design#
The callback queues are input, animation, traversal, commit. The exact list has grown over the years, but the principle has not: a frame is a pipeline of dependent stages, and running them in a fixed order inside one interval is what makes the result internally consistent.
If animations ran after traversal, a view would be laid out at its old position and drawn at its new one — for one frame, visible as a flicker. If input ran after animation, a touch that starts a drag would be applied a frame late. The ordering is not incidental; it is the contract.
What "Project Butter" means today#
The name survives in the Android source as a comment in places, and the design survives everywhere. Every frame on a modern phone is still scheduled by Choreographer against a vsync, still recorded on the UI thread and replayed on the RenderThread, and still handed through a multi-buffer queue.
What changed since 2012 is the clock itself. Adaptive refresh means the interval is no longer a constant, and frame pacing has to decide which rate to run at based on what is on screen. The scheduling idea Butter introduced is what makes that possible: once frames are aligned to a clock, changing the clock is a policy decision rather than a rewrite.
The counterfactual#
If Project Butter had only added the vsync clock and not the buffer, Android would have had a system that knew when frames were due and could not meet the deadline — a scheduling framework that made lateness visible without making it rarer. If it had only added the buffer, frames would have been smoother without being aligned to the display, which is the situation before 2012 with more memory.
The two changes are a pair. That is the most transferable lesson in the whole story: a deadline is only useful if there is slack to meet it.
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 |
|
The line from 2012 to today is an unbroken extension of the same idea: give every stage a deadline, and make the display's clock authoritative. Adaptive refresh rates complicate it — the deadline now depends on what the panel is doing — but they do not change the principle.
Under the hood#
- Choreographer.javathe frame-callback queues and the doFrame loop, the direct descendant of the 4.1 design
- today's vsync distribution and frame pacing
- BufferQueueProducer.cppwhere the buffer count turns a deadline into a blocking or non-blocking dequeue
Curiosity corner#
- The name was a joke that stuck. The project's internal framing was that Android's UI was "dry" and needed butter. The Google I/O 2012 talk title was "For Butter or Worse".4
- Butter shipped in the same release as expandable notifications, which is why 4.1 is remembered for both smoothness and the notification shade — two very different kinds of polish in one release.
- The Choreographer API is still how animations are scheduled. Compose's frame clock sits on top of the same mechanism, more than a decade later.
Try it#
adb shell dumpsys display | grep -i "refresh-rate|fps"Example output
1080x2400
refresh-rate: 120.00001
mode: {id=1, width=1080, height=2400, fps=120.0}adb shell dumpsys gfxinfo com.example.appExample output
Total frames rendered: 912
Janky frames: 14 (1.53%)Check 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.
- docsImplement VSYNCsource.android.comaccessed 2026-10-27↩
- docsGraphics architecturesource.android.comaccessed 2026-10-27↩
- docsBufferQueue and grallocsource.android.comaccessed 2026-10-27↩
- talkFor Butter or Worse (Google I/O 2012)Google I/Oaccessed 2026-10-27↩
- docsRendering | App qualitydeveloper.android.comaccessed 2026-10-27↩
- wikiAndroid version historyWikipediaaccessed 2026-10-27↩