Zygote
Graphicsintermediate

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.

12 minAPI 1–37 · 17interactivequizverified 2026-10-27

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#

Loading diagram…
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.

3
Loading diagram…
Text 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 1–API 15Free-runningdraw when ready; compositor takes the newest bufferAPI 1
API 16–API 20Vsync + triple bufferProject Butter: scheduled frames, ChoreographerAPI 16
API 21 →RenderThreadGPU work moves off the UI threadAPI 21
Interface changes16 releases
APIVersionWhat changed
11.0
  • Pull-down notification shade
  • Home screen with widgets
31.5 Cupcake
  • Home screen widgets and folders
92.3 Gingerbread
  • New UI with dark theme and green accents
113.0 Honeycomb
  • The Holo design language arrives
123.1 Honeycomb
  • Resizable widgets
164.1 Jelly Bean
  • Project Butter: vsync-driven, smooth UI
  • Expandable notifications
204.4W KitKat (Wear)
  • Voice-driven card UI
215.0 Lollipop
  • Material Design
  • Notifications on the lock screen
247.0 Nougat
  • Quick Settings tiles
  • Direct Reply in notifications
268.0 Oreo
  • Notification channels
289 Pie
  • Dark theme groundwork
2910
  • System-wide dark theme
3011
  • Conversation notifications and bubbles
  • Media controls in Quick Settings
3112
  • Material You dynamic colour
3313 Tiramisu
  • Themed app icons
  • Runtime notification permission
3616 Baklava
  • Material 3 Expressive
  • Live Updates notifications

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#

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#

Check the display's refresh rate
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}
Frames, jank and percentiles
adb shell dumpsys gfxinfo com.example.app
Example output
Total frames rendered: 912
Janky frames: 14 (1.53%)

Check yourself#

Check yourself

1 / 12

At 120 Hz, how much time does one frame have before it misses the display's deadline?

0 answered · 0 correct

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.

  1. docsImplement VSYNCsource.android.comaccessed 2026-10-27↩
  2. docsGraphics architecturesource.android.comaccessed 2026-10-27↩
  3. docsBufferQueue and grallocsource.android.comaccessed 2026-10-27↩
  4. talkFor Butter or Worse (Google I/O 2012)Google I/Oaccessed 2026-10-27↩
  5. docsRendering | App qualitydeveloper.android.comaccessed 2026-10-27↩
  6. wikiAndroid version historyWikipediaaccessed 2026-10-27↩