Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 1.4Beginner ~23 min · shot 4 of 6

What the Browser Does Each Frame

In one line

To make one frame the browser runs your scripts, works out styles, lays the page out, paints it and composites the layers. One main thread does most of that, one at a time, while a second thread, the compositor, can keep already-painted layers moving. That split decides which animations stay smooth when the page is busy.

0/6 · 0%

Think of it like this

A theatre. The stage manager (the main thread) reads every cue, moves scenery, changes the lights and talks to the crew, one job at a time. A second person, the follow-spot operator (the compositor), only swings a spotlight along a path that is already set up. If the stage manager gets stuck for ten seconds on a hard cue, the spotlight keeps moving. Everything that needs the stage manager waits.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

main thread
The one line of work that runs your JavaScript, style, layout and most painting, one job at a time.
compositor
A separate thread that puts painted layers together and can move, scale and fade them without the main thread.
layer
A picture of part of the page that the compositor can move or fade as a whole.
style
The step that works out which CSS rules apply to each element and what the final values are.
layout
The step that works out the size and position of every element.
paint
The step that draws an element's pixels: text, colours, borders and shadows.
long task
A job on the main thread that runs longer than 50 ms. Nothing else can happen on the page meanwhile.

Step by step

01Open the lanes

The pipeline lanes widget below shows eight frames of one animated property, split into two lanes. The upper lane is the main thread and the lower lane is the compositor. Each frame lists the steps that run in each lane. The times are illustrative, not measured: the point is which lane does what.

02Compare the properties

Start with transform: it needs the least work. Switch to left or width: every frame needs style, layout, paint and composite, all on the main thread. box-shadow needs style and paint, but not layout. The widget also draws background-color as paint. That is a simplification: in the Chromium we measured, an animated background-color is composited (Topic 6.2).

03Switch the main thread to busy

Turn on main thread busy (45 ms). For transform the film strip keeps moving, frame after frame. For left, width, box-shadow (and the widget's background-color) frames are missed: the strip repeats the same picture.

04Do it for real

Press Replay under "A long task", leave the speed at 1×, and press the button. The orange dot stops. The blue dot keeps moving. The meter reads about 600 ms. Do not press Pause before the button: Pause takes over the page's clock (Topic 0.3) and the dots no longer run natively.

05Run the order of a frame

"The order of things" prints five lines. The script runs to the end. Then the microtask (promise callbacks run as soon as the current script finishes). Then the requestAnimationFrame callback, just before the browser makes the next frame. Then the nested one, one frame later. A change you make inside a requestAnimationFrame callback is therefore in the very next picture.

Run the order of a framediagram
Rendering diagram…

Try it yourself

  1. 1

    Block for longer

    In "A long task" change 600 in the click handler to 100. Press Replay, then the button. The orange dot stops for a moment and the meter reads about 100 ms (measured: 100 ms), about five lost frames. On a 60 Hz screen that is a visible stall. Now try 30: the meter reads about 33 ms, one lost frame. Most people do not notice a single 30 ms hole.

  2. 2

    Add a third rAF

    In "The order of things" add a third nested requestAnimationFrame that prints 6. after the second one. It prints last, in a third frame. Each nested call waits one frame, so nested calls spread work across frames.

  3. 3

    Skip the style read

    In "Style is worked out when the browser looks" delete void b.offsetWidth;. Line B now prints 0: for the browser, B was never styled before the class changed, so it has no old value to animate from. This is the reason for the void el.offsetWidth line some code uses to restart an animation.

Code & diagrams

Try it: interactive

Pipeline lanes

Pick an animated property, then turn on "main thread busy". Which properties keep moving? The stage times are illustrative, not measured.

A long task

Press the button with the speed at 1× and the page not paused. Orange needs the main thread for every frame. Blue is a transform animation, which the compositor can keep moving.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.
The order of things

Plain console output: scripts, then microtasks, then the callbacks that run before the next frame.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

1. script starts
2. script ends
3. microtask: runs as soon as this script ends
4. rAF callback: runs just before the next frame is made
5. rAF callback of the frame after that
Style is worked out when the browser looks

A transition starts only when the browser sees a change between two style calculations. Reading a size forces a calculation at that point (Phase 6 shows the cost of this).

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

A new element, class added at once : 0
B new element, styles read first    : 1
C class added, then removed         : 0

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Move the blue dot with left instead of transform

In "A long task" change the CSS keyframes to @keyframes slide { to { left: 200px; } }, add position: relative to .dot.blue, press Replay and then the button.

terminal
$ Press Replay, then press the button, and watch both dots
── what you'll see ──
Both dots stop for the whole block. Measured in a real Chromium run with a 1,000 ms block: no frame at all reached the screen between 519 ms and 1,569 ms.
Same page, only the animated property changed.

Myth vs fact

Myth

CSS animations are always smooth and JavaScript animations are always slow.

Fact

What matters is the property and the thread. A CSS animation of width runs on the main thread and stutters when it is busy. A JavaScript animation can be smooth when the main thread is free. transform and opacity can run on the compositor, from CSS or from the Web Animations API. Chromium 153 (measured, headless) also composites background-color and clip-path, but left, width, box-shadow and filter: blur() still need the main thread. Check the property, not the habit.

Myth

If my animation is on the compositor, the main thread cannot hurt it.

Fact

It can be delayed at the start, because the main thread decides when to hand the animation over. And any handler that waits for the animation to end (animationend, finished) is delayed until the main thread is free.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The five steps are the same ones DevTools names in the Performance panel (Scripting, Rendering for style and layout, Painting, and the compositor's frames). Phase 6.5 shows how to read them. The names differ a little between browsers, but the order does not.

  • ▸

    Layers cost memory (width times height times 4 bytes of pixels), so a layer for every element is not free. Topic 6.3 covers will-change.

Remember this

  1. 1

    The problem this solves: an animation that looked fine yesterday stutters today, and nothing in its code changed. Usually the page got busy somewhere else. To predict which animations survive a busy page, you need a picture of what the browser does each frame, and which part of it runs where.

  2. 2

    The five steps of a frame. (1) JavaScript: event handlers, timers and requestAnimationFrame callbacks run, and may change the page. (2) Style: the browser works out which CSS rules apply to each element, and their final values. (3) Layout: it works out each element's size and position. (4) Paint: it draws each element's pixels (text, colours, borders, shadows) into pictures. (5) Composite: it puts the pictures, called layers, together into the final frame, moving, rotating and fading them as needed, and sends it to the screen.

  3. 3

    The main thread does steps 1 to 4, one job at a time: a click handler, then a timer, then the frame's work. A thread is one line of work that a program runs. While the main thread is busy with one job, the rest wait in a queue. A job that runs more than 50 ms is called a long task. During a long task there are no frames, no clicks handled and no scrolling that depends on the main thread: the page is frozen.

  4. 4

    The compositor thread does step 5 on its own thread, mostly on the GPU (the graphics chip). It already has the painted layers. Moving, scaling, rotating or fading a layer needs no new painting and no layout, so the compositor can do it without asking the main thread. That is why a CSS animation of transform or opacity can stay smooth while the main thread is stuck. Changing left or width needs layout, and changing box-shadow or a text color needs paint, so the main thread must do the work every frame. Which properties the compositor can take over differs between browsers and versions (Topic 6.2 gives the table we measured in Chromium).

  5. 5

    Measured on a real run. In the example below, an orange dot is moved by JavaScript on every frame and a blue dot by a CSS transform animation. Pressing the button runs a 600 ms loop on the main thread. In a Chromium run recorded frame by frame (a similar page, with a 1,000 ms block): the orange dot stood still for about 1,000 ms, while the blue dot moved on every 16 ms frame. Then the orange dot jumped ahead, because it was time-based (Topic 1.3) and caught up with where the clock said it should be. On this page, a 600 ms block made the meter read 583 ms.

  6. 6

    One job per frame is enough to ruin it. A 50 ms job on a 60 Hz screen costs three frames. And your own JavaScript is not the only source of long tasks: parsing a big JSON file, laying out a huge list, or a slow third-party script. The animation does not need to be the thing that is slow.

Explain it without notes

01

Name the steps of a frame in order, and say which thread runs which.

02

Why can a transform animation keep moving while the page is frozen by a long task, but a left animation cannot?

Practice

01

A button's click handler sorts 50,000 items (about 80 ms). Say what happens to a spinner built with a JavaScript loop, and to a spinner built with a CSS transform: rotate animation, while it runs.

02

Rewrite this to use the compositor: box.style.left = x + "px" on every requestAnimationFrame.

Done when you can

  • You can list the five steps of a frame in order

  • You can say what the main thread and the compositor each do

  • You can say which properties can move on the compositor

  • You can explain what a long task does to animations