awesome-everything RU
↑ Back to the climb

Browser & Frontend Runtime

BeginMainFrame, compositor-driven animations, and GPU memory

Crux How the Chromium two-thread handshake works, why CSS animations on transform/opacity survive a blocked main thread, and what happens when GPU memory is exhausted.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min

A CSS animation keeps running smoothly at 60 fps while the main thread is blocked for 400 ms parsing a JSON blob. A rAF animation on the same element freezes for the same 400 ms. Same hardware, same property, opposite results — because one hands control to the compositor, the other does not.

The BeginMainFrame handshake

In Chromium’s renderer process, the compositor thread is the driver and the main thread is the worker. The flow each vsync:

  1. Compositor sends BeginMainFrame to the main thread
  2. Main thread runs all per-frame work: rAF callbacks, style, layout, paint setup
  3. Main thread sends CommitMainFrame back, handing off a new layer tree
  4. Compositor asks raster workers to fill any dirty tiles with bitmaps
  5. Compositor performs the GPU draw and displays the frame

If the main thread is too slow, the compositor does not wait: it ships the previous frame’s layer tree again (a “drawn but stale” frame). The user perceives this as a missed input or a stutter.

BeginMainFrame arrives every ~16.67 ms whether the main thread is ready or not. This is the deep reason main-thread work cannot exceed the frame budget: the metronome is relentless.

BeginMainFrame flow

CompositorBeginMainFrame →
Main threadrAF + style + layout + paint setup
Main thread← CommitMainFrame
Compositorraster workers fill dirty tiles → GPU draw
If main thread misses: compositor ships previous frame (“stale frame”)

Compositor-driven CSS animations

When you write a CSS animation that only changes transform or opacity on a compositor-promoted element, the browser detects this at animation-start and hands the animation’s timeline directly to the compositor thread. From that point the main thread is not involved at all — the compositor interpolates the value frame-by-frame and renders the next bitmap directly.

This is why a CSS animation can keep running smoothly even when the main thread is completely blocked (alert dialog, long synchronous parse, debugger pause): the compositor still runs.

The same animation written in JS via rAF cannot do this, because rAF runs on the main thread. A blocked main thread freezes the rAF animation.

Compositor-driven CSS animations are the only way to guarantee animation continuity under main-thread pressure.

GPU memory exhaustion and layer eviction

Layers are not free. Each layer is a GPU bitmap costing width × height × 4 bytes. On a phone with 256 MB of GPU memory, fifty 1080p layers exhausts the budget. When the OS starts evicting tiles:

  1. Browser detects the evicted tiles
  2. Raster workers re-rasterise them on the fly
  3. While rasterising, the compositor composites with incomplete tiles
  4. The page stutters — often worse than if no layers were requested at all

The will-change anti-pattern causes this: will-change: transform on every component permanently reserves a layer per instance. A list of 100 cards each with 5 will-change’d sub-elements holds 500 layers.

The correct pattern: set will-change just before the animation starts (on mouseenter, or in a transitionstart listener), remove it on animationend or transitionend. This keeps GPU memory near zero when the user is not actively interacting.

Why this works

Why does will-change: transform cause promotion before the animation even starts? The browser needs time to rasterise the layer into GPU memory before the first frame of the animation. If it waited until the animation started, the first 1–3 frames would be blank while rasterisation ran. will-change is the hint that says “prepare now, not at animation-start.” The cost is holding the bitmap for the duration of the hint — which is why you want to remove the hint when the animation ends.

Pick the best fit

Animate a card from y=0 to y=200 over 300 ms at 60 fps. Pick the implementation.

Quiz

The main thread is blocked for 400 ms by a synchronous JSON.parse. Which kind of animation continues to run smoothly at 60 fps during the block?

Quiz

GPU memory balloons on a mobile device. Profiling shows 500 compositor layers. Most are from cards in a list, each with `will-change: transform` set permanently on mount. What is the targeted fix?

Recall before you leave
  1. 01
    What does the compositor do if the main thread misses a BeginMainFrame deadline?
  2. 02
    Why does a CSS transform animation survive a blocked main thread while a rAF animation does not?
  3. 03
    What is the will-change anti-pattern and how do you fix it?
Recap

The Chromium compositor sends BeginMainFrame every ~16.67 ms. The main thread responds with CommitMainFrame if it finishes on time; if not, the compositor ships the previous frame as a stale frame. CSS animations on transform or opacity of promoted elements are handed to the compositor at animation-start — the main thread is out of the loop, so they survive a blocked main thread at 60 fps. rAF animations run on the main thread and freeze when it is blocked. The will-change anti-pattern reserves GPU bitmaps permanently; on a phone with 256 MB GPU memory, 500 layers from a design system exhausts the budget and the OS evicts tiles. Scope will-change to the duration of an animation to keep GPU memory near zero at rest.

Connected lessons
appears again in143
Continue the climb ↑Production observability: LoAF, INP, and the full attack surface
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.