completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

The event loop and frames

Tasks, microtasks, and rendering opportunities: when the browser can paint and what stops it. Measured in Chrome: the order callbacks run in, real frames, and a long task's gap.

By the end, you can
  • 01
    Predict the orderSay when a script, a timer, a promise and a frame callback run, and why microtasks come before the next task.
  • 02
    Follow one frameExplain when the browser gets a chance to draw, what it does in each frame, and how long a frame lasts at 60 and 120 Hz.
  • 03
    See what a long task costsShow, with frames Chrome recorded, how one long task holds up every frame and every tap until it ends.

One loop, one thread

Asha is on the Leaf and Kettle page again. Near the top, a small tea leaf drifts across the banner. A script moves it a tiny step in every frame. A frame is one picture on the screen. The screen shows many of them each second, like the pages of a flip book.

Asha taps Add to cart. The shop's click handler is slow: it keeps the browser busy for 300 ms. For that moment, the leaf stops. Then it carries on.

The course made a copy of this banner and tapped its Add to cart button in Chrome. Here is what the screen showed:

What Asha's screen showed, every 150 ms, in real frames Chrome drew. Times count from when the click handler started. At -150 ms, the leaf drifts: the leaf is 128 px from the left of the banner. At 0 ms, Asha taps: the leaf is 144 px from the left of the banner. At 150 ms, the handler runs: the leaf is 146 px from the left of the banner. At 300 ms, the handler ends: the leaf is 146 px from the left of the banner. At 450 ms, the leaf moves on: the leaf is 167 px from the left of the banner. A shaded bar beside the screens from 0 ms to 300 ms is the click handler, and a dashed line runs through the leaf on those screens.-150 msThe banner -150 ms after the click handler started: the tea leaf, and the Add to cart buttonthe leaf drifts0 msThe banner 0 ms after the click handler started: the tea leaf, and the Add to cart buttonAsha taps150 msThe banner 150 ms after the click handler started: the tea leaf, and the Add to cart buttonthe handler runs300 msThe banner 300 ms after the click handler started: the tea leaf, and the Add to cart buttonthe handler ends450 msThe banner 450 ms after the click handler started: the tea leaf, and the Add to cart buttonthe leaf moves on
Asha's screen every 150 ms, in real frames Chrome drew when the course tapped Add to cart on a copy of the banner. Times count from when the click handler started; the shaded bar is the handler. After one last frame 13 ms in, no new frame came until 305 ms, so the leaf stayed on the dashed line.

While the click handler ran, the leaf stayed on the dashed line. It moved on only once the handler had finished.

Why did the leaf stop? The click had nothing to do with the leaf. And how can a slow script stop the drawing at all?

To find out, the course built a small test page that does the same two things. A function moves things in every frame. Halfway through, one job keeps the browser busy for 300 ms. The course recorded the page in Chrome 156. Here is one frame from that recording:

A frame that had to waitPop out in the code editor (opens in a new tab)JavaScript
// One frame on a test page, as Chrome recorded it. Times are in milliseconds (ms).const frame = { began: 1519.5, callbackRan: 1811.3 }; console.log(`The frame began at ${frame.began} ms.`);console.log(`Its callback ran at ${frame.callbackRan} ms.`);console.log(`It waited ${(frame.callbackRan - frame.began).toFixed(1)} ms.`);
  • Line 1 is a comment. Times are in milliseconds (ms), counted from when the page started loading. A millisecond is a thousandth of a second.
  • Line 2 holds two times. began is when the frame began. callbackRan is when the page's code for that frame ran. That code is a frame callback: a function the page asks the browser to run just before it draws a frame.
  • Lines 4 to 6 print The frame began at 1519.5 ms., Its callback ran at 1811.3 ms. and It waited 291.8 ms.

Most frames on this page ran their callback about 2 ms after they began. This one waited almost 300 ms. While it waited, nothing on the page moved. Something was in the way. The rest of this lesson finds out what.

The answer starts with lesson 6.1. One thread, the main thread, runs almost everything a page does: its JavaScript, its styles, its layout and its paint. A thread can only do one thing at a time. So the main thread needs a rule for what to do next. Lesson 6.1 compared it to a head cook. Here is how she picks her next dish:

Real-life analogyThe head cook's ticket rail

Orders reach the kitchen as paper tickets. They hang on a rail in the order they came in. The cook takes the oldest ticket and makes that order from start to finish. Only then does she take the next one. If one order takes ten minutes, every ticket behind it waits ten minutes.

In real life: The cook
In the browser: The main thread
In real life: One ticket: one order to make
In the browser: One task, like running a script or a click handler
In real life: The rail of tickets, oldest first
In the browser: A task queue
In real life: Take the oldest ticket, finish it, take the next
In the browser: The event loop

Now the same thing in the browser's words. A task is one job the main thread runs from start to end. Running a script is a task. So is a timer's callback, and so is a click handler. Tasks wait in a task queue, in the order they arrived. The event loop is the rule the main thread follows, over and over:

  1. Take the oldest task from a task queue.
  2. Run it, all the way to the end.
  3. Run every microtask that is waiting. A microtask is a small follow-up job. The next section explains them.
  4. Go back to step 1.

There's no step for drawing. That's on purpose. In the HTML Standard, drawing a new frame is a task too. When the screen is ready for a new picture, the browser queues a task called update the rendering. It waits its turn, like the click and the timer. Here is the whole loop in one picture:

The event loop. On the left, tasks wait in line: a click, a timer, and update the rendering. An arrow takes the oldest task to the step: run the oldest task. The next step is: run every microtask. A curved arrow goes back for the next task. A note says the update the rendering task joins the line at each rendering opportunity.tasks waitinga clicka timerupdate the renderingrun the oldest taskrun every microtaskthen the next taskjoins at eachrendering opportunity
One turn of the loop: run the oldest task, then every microtask, then go back for the next task. Drawing a frame is a task too, and it waits its turn.
The main idea

The main thread runs one task at a time, then every waiting microtask. Drawing a frame is a task too, so it can only happen between other tasks. A long task holds up every frame, and every tap, until it ends.

That already answers Asha's question. Her leaf's frame was a task, stuck behind a 300 ms task. The next sections go one step at a time: microtasks first, then when frames come, what happens inside one, how long it may take, and what a long task does. The JavaScript lesson Rendering & the event loop shows the loop with small programs. This lesson times it in Chrome.

Tasks and microtasks

Start with the simplest case, a timer. setTimeout(fn, 0) asks the browser to run fn after 0 ms. Does that mean right away?

A 0 ms timer waits its turnPop out in the code editor (opens in a new tab)JavaScript
setTimeout(() => console.log("The timer's callback ran."), 0);
console.log("The script finished.");
  • Line 1 sets a timer with a delay of 0 ms. The arrow function is its callback: a function you hand over for someone else to run later. When the delay is up, the browser queues a task that will run it.
  • Line 2 prints The script finished.
  • Then the timer's task gets its turn, and prints The timer's callback ran.

The script itself is a task. The main thread finishes it before it takes the next one. A delay of 0 doesn't mean "now". It means "queue a task as soon as you can". Press Visualize to watch the timer's task wait in its queue.

Tasks come from many places. MDN lists these:

  • Running a <script> as the page loads.
  • A click: the browser makes a task that runs the click handlers.
  • A timer from setTimeout() or setInterval(), when its time is up.

A browser can keep more than one task queue. The HTML Standard's example is one queue for clicks and key presses, and one for everything else. The browser picks which queue to serve next, so it can serve clicks and key presses first.

Microtasks run before the next task

Some follow-up jobs don't wait for the next turn of the loop at all. Back in the kitchen:

Real-life analogyNotes stuck to the ticket

Some tickets come with a sticky note: "while you're at it, warm the cups". The cook does every note on a ticket before she takes the next ticket. If a note says "and add a note to warm the plates", she does that one too. Only when no notes are left does she reach for the rail.

In real life: A sticky note on the ticket
In the browser: A microtask, like a promise callback
In real life: Doing every note before the next ticket
In the browser: The microtask checkpoint after each task
In real life: A note that adds another note
In the browser: A microtask that queues another microtask
In real life: The next ticket on the rail
In the browser: The next task, such as a timer

A microtask is a small job that runs as soon as the current task ends, before the loop takes the next task. Three kinds are common:

  • the callback you pass to a promise's .then();
  • the rest of an async function, after an await;
  • a function you pass to queueMicrotask().

Microtasks wait in their own line, the microtask queue. After every task, the loop empties it. The HTML Standard calls that a microtask checkpoint. Here is a promise next to a timer:

A promise callback jumps ahead of a timerPop out in the code editor (opens in a new tab)JavaScript
setTimeout(() => console.log("The timer ran: a new task."), 0);Promise.resolve().then(() => console.log("The promise callback ran: a microtask."));console.log("The script finished.");
  • Line 1 sets a timer. Its callback will run in a new task, later.
  • Line 2 makes a promise that has already succeeded, with Promise.resolve(). So its .then() callback goes straight into the microtask queue.
  • Line 3 prints The script finished.
  • The script's task ends, so the loop empties the microtask queue. It prints The promise callback ran: a microtask.
  • Only then does the loop take the timer's task: The timer ran: a new task.

The promise was queued after the timer, but it ran first. A microtask always beats the next task.

What if a microtask queues another microtask? MDN says the loop "will keep calling microtasks until there are none left in the queue, even if more keep getting added." Here it is:

A microtask that adds a microtaskPop out in the code editor (opens in a new tab)JavaScript
setTimeout(() => console.log("The timer ran."), 0);queueMicrotask(() => {  console.log("The first microtask ran.");  queueMicrotask(() => console.log("The microtask it added ran too."));});console.log("The script finished.");
  • Line 1 sets a timer.
  • Lines 2 to 5 queue a microtask with queueMicrotask(). When it runs, line 3 prints The first microtask ran. Then line 4 queues a second microtask.
  • Line 6 prints The script finished.
  • The rest comes out as The first microtask ran., The microtask it added ran too. and The timer ran.

The second microtask didn't exist when the script ended. It still ran before the timer. That's the note that adds a note.

Why it matters: microtasks are quick to start, but they still use the main thread. They all run before the next task, and drawing a frame is a task. MDN warns that microtasks which keep adding microtasks can keep the loop busy "endlessly".

Now add a frame callback to the mix. The course ran the five lines below from one task in Chrome, five times. Every run went script, promise, queueMicrotask, setTimeout, requestAnimationFrame. You can predict the first three. The last two are a race: the timer waits for its task's turn, and the frame callback waits for the next frame. Run it a few times in your own browser:

Playground: who runs first?
The five linesJavaScript
setTimeout(() => log("setTimeout"), 0);requestAnimationFrame(() => log("requestAnimationFrame"));Promise.resolve().then(() => log("promise"));queueMicrotask(() => log("queueMicrotask"));log("script");
Not run yetlive
    Try it yourself
    The order

    Run the five lines in your browser. Your click is the task that queues them all.

    This runs in your browser. log() writes down each callback's name, and how long after your click it ran.

    Rendering opportunities and vsync

    So when does the frame's turn come? Not whenever the page likes. The screen sets the beat.

    A screen shows a new picture at a fixed rate, its refresh rate. It's counted in hertz (Hz), which means times a second. A 60 Hz screen shows 60 pictures a second, one every 16.7 ms. Each picture is a frame, one page of the flip book.

    The display sends a signal for each new picture. That signal is called vsync. Chrome's source says the call to begin each frame "normally" comes "just after a vsync interrupt".

    The HTML Standard calls each chance to show a new frame a rendering opportunity. What it says about them:

    • They depend on "hardware refresh rate constraints and user agent throttling for performance reasons."
    • At 60 Hz, they come "at a maximum of every 60th of a second (about 16.7ms)."
    • A page that can't keep up might get 30 a second instead.

    At each rendering opportunity, the browser notes the time and queues an update-the-rendering task. In the kitchen:

    Real-life analogyThe waiter's rounds

    A waiter passes the kitchen hatch on a steady round, once a minute. Each time, he clips a ticket to the rail: "plate up the next course". The cook gets to it when she finishes the ticket in her hands. If she's quick, the plate goes out on time. If she's stuck on a big order, the plate waits, and his next rounds bring nothing new to the table.

    In real life: The waiter
    In the browser: The compositor thread from lesson 6.1
    In real life: His steady round, once a minute
    In the browser: The screen's refresh: every 16.7 ms at 60 Hz
    In real life: Each pass by the kitchen hatch
    In the browser: A rendering opportunity
    In real life: His ticket: "plate up the next course"
    In the browser: The update-the-rendering task
    In real life: The plate on the table
    In the browser: A new frame on the screen

    In Chrome, the waiter is the compositor thread from lesson 6.1. Chromium's How cc works describes the hand-off:

    1. The screen's beat reaches the compositor thread.
    2. If the page has asked for a frame, for example with requestAnimationFrame(), the compositor thread sends the main thread a message to begin a frame. The message carries the frame's time.
    3. The main thread runs the frame callbacks, then the rest of the frame's work, and hands the result back.

    If the main thread is slow to answer, the docs say, the compositor "may draw without waiting" for it.

    How steady is the beat? The course's Chrome ran headless, with no real screen, and still kept a steady beat. Here are six frames in a row from the test page:

    Six frames on the screen's beatPop out in the code editor (opens in a new tab)JavaScript
    // When six frames in a row began, in ms, as Chrome recorded them.const frames = [1436.1, 1452.8, 1469.4, 1486.1, 1502.8, 1519.5]; const frame = (frames[5] - frames[0]) / 5;console.log(`A new frame began every ${frame.toFixed(1)} ms.`);console.log(`That's ${Math.round(1000 / frame)} frames a second.`);
    • Line 2 holds when each of six frames began, in a row.
    • Line 4 finds one gap. From the first frame to the sixth there are five gaps, so it divides by 5.
    • Line 5 prints A new frame began every 16.7 ms. Line 6 prints That's 60 frames a second.

    Why it matters: the beat doesn't wait for the page. A frame that isn't ready on one beat can only go out on a later one, 16.7 ms or more later.

    requestAnimationFrame timing

    The leaf moves because its page asks for a frame callback. requestAnimationFrame() asks the browser "to call a user-supplied callback function before the next repaint", in MDN's words. The next repaint is the next frame.

    Here is the frame callback from the course's second test page, the grid page. The grid is a box of 2,000 small squares. Each frame, the callback makes the box a little wider, so the squares move to new places:

    The grid page's frame callbackJavaScript
    // The grid page's frame callback. grid is a box of 2,000 small squares.let width = 300;const grow = () => {  width = width >= 700 ? 300 : width + 4;  grid.style.width = width + "px";  requestAnimationFrame(grow);};requestAnimationFrame(grow);
    • Line 1 is a comment. grid is the box, found earlier on the page.
    • Line 2 starts the width at 300 pixels.
    • Lines 3 to 7 make grow, the frame callback. Line 4 adds 4 pixels to the width, and goes back to 300 after 700. Line 5 gives the box its new width.
    • Line 6 asks for grow again, for the next frame. Line 8 asks for the first frame.

    Each call to requestAnimationFrame() books one callback, for one frame. MDN calls it "one-shot". That's why line 6 asks again. Without it, the grid would grow once and stop.

    So what does the browser do inside that frame? The course recorded the grid page with Chrome's tracing on. A trace is a detailed log of everything the browser did, step by step, with times. From it, the course took one typical frame:

    One recorded frame of the grid page on the main thread: Frame callbacks 0.1 ms, then Recalculate style 0 ms, then Layout 0.8 ms, then Paint 0.5 ms, then Commit 0 ms.callbacks0.1 msstyle0.0 mslayout0.8 mspaint0.5 mscommit0.0 ms
    One frame of the grid page, from Chrome's trace. The frame callbacks run first, so the width they change is styled, laid out and painted in the same frame. Two tiny steps, pre-paint and layerize, are left out.
    The steps of one frame, in orderPop out in the code editor (opens in a new tab)JavaScript
    // The steps of one frame on the grid page, in the order Chrome's trace shows them.const steps = ["Frame callbacks", "Recalculate style", "Layout", "Pre-paint", "Paint", "Layerize", "Commit"];console.log(`First: ${steps[0]}.`);console.log(`Then: ${steps.slice(1).join(", ")}.`);
    • Line 2 lists the frame's steps on the main thread, in the order the trace shows them.
    • Line 3 prints First: Frame callbacks.
    • Line 4 prints Then: Recalculate style, Layout, Pre-paint, Paint, Layerize, Commit.

    Here is what each step does:

    • Frame callbacks: Chrome runs grow, and any other requestAnimationFrame() callbacks.
    • Recalculate style: it works out which CSS rules apply to each element, and their final values.
    • Layout: it works out where each square goes, and how big it is. The box is wider, so squares move.
    • Pre-paint: it marks what needs painting again.
    • Paint: it makes a list of what to draw, and in what order.
    • Layerize: it groups what was painted into layers, parts of the page drawn separately.
    • Commit: it hands the result to the compositor thread.

    The next lesson, The pixel pipeline, looks at style, layout, paint and layers in detail. For now, notice the order. The frame callbacks run first. So the new width from line 5 is styled, laid out and painted in this same frame. The HTML Standard's update-the-rendering steps have the same order: "run the animation frame callbacks", then "Recalculate styles and update layout".

    Each callback also gets a timestamp: the time its frame began. The HTML Standard sets it to the moment of the rendering opportunity. MDN calls it "the end time of the previous frame's rendering". It's the began from the start of the lesson. On the test page, a callback usually ran 2.2 ms after its frame's timestamp.

    Why it matters: make visual changes in a frame callback. In web.dev's Optimize JavaScript execution, Paul Lewis calls requestAnimationFrame() "the only way to guarantee that your JavaScript will run at the start of a frame". A setTimeout() callback runs "at some point in the frame, possibly right at the end". Then the page can miss a frame. It's like handing the waiter a plate just after he has left the hatch.

    Frame budgets at 60 and 120 Hz

    How much time does one frame give you? A frame budget is how long one frame lasts: 1000 ms divided by the refresh rate. MDN says 60 Hz is the most common refresh rate, "though 75hz, 120hz, and 144hz are also widely used."

    How long a frame lastsPop out in the code editor (opens in a new tab)JavaScript
    for (const hz of [60, 75, 120, 144]) {  console.log(`At ${hz} Hz, a frame lasts ${(1000 / hz).toFixed(1)} ms.`);}
    • Line 1 goes through four refresh rates, one at a time.
    • Line 2 divides 1000 ms, one second, by the rate. It prints At 60 Hz, a frame lasts 16.7 ms. and At 75 Hz, a frame lasts 13.3 ms.
    • Then At 120 Hz, a frame lasts 8.3 ms. and At 144 Hz, a frame lasts 6.9 ms.

    A frame budget works like a bus timetable. The bus leaves every 16.7 ms, full or not. Work that isn't ready waits for the next bus.

    Not all of the budget is yours, either. Paul Lewis explains why in web.dev's Rendering performance. At 60 Hz, he says, "the browser has its own overhead for each frame, so all of your work needs to be completed inside 10 milliseconds." Miss it, and "the frame rate drops, and page contents judder on-screen." That judder is called jank.

    How much of its budget did the grid page use? Here are the times of its frame's seven steps:

    The grid page's frame, against the budgetPop out in the code editor (opens in a new tab)JavaScript
    // How long each of the frame's seven steps took, in ms, in the same order.const steps = [0.1, 0, 0.8, 0.1, 0.5, 0, 0]; const work = steps.reduce((sum, ms) => sum + ms, 0);console.log(`The seven steps took ${work.toFixed(1)} ms in all.`);console.log(`That's ${Math.round((work / (1000 / 60)) * 100)}% of a frame at 60 Hz.`);console.log(`And ${Math.round((work / (1000 / 120)) * 100)}% of a frame at 120 Hz.`);
    • Line 2 holds how long each step took, in the same order as the list above.
    • Line 4 adds them up. Line 5 prints The seven steps took 1.5 ms in all.
    • Lines 6 and 7 compare that with one frame. They print That's 9% of a frame at 60 Hz. and And 18% of a frame at 120 Hz.

    The grid page fits easily. But look at the second share. The same work takes twice the share of a 120 Hz frame. On a faster screen, the budget halves.

    A faster screen changes animations too. The grid page adds 4 pixels every frame. A 120 Hz screen has twice as many frames a second, so the grid would grow twice as fast. MDN warns about this: use the timestamp to work out how far to move, "otherwise, the animation will run faster on high refresh-rate screens."

    Measure your own screen. The playground records each frame's timestamp for two seconds, as the course's test page did. Then try it with a long task in the middle:

    Playground: your screen's frames
    Frames0 to 2 s
    Your screenlive
    Framesnot yet
    A frame everynot yet
    Frames a secondnot yet
    Longest gapnot yet
    Try it yourself
    Your frames

    Measure your own screen. The playground writes down each frame's timestamp for two seconds. Then try it with a long task halfway.

    This runs in your browser, with requestAnimationFrame. Each tick is one frame's timestamp, over 2 seconds. The shaded stretch is the task: a loop that keeps the main thread busy for 300 ms.

    Why a long task freezes everything

    Back to Asha's leaf, and the frame that waited 291.8 ms. web.dev's Optimize long tasks puts the rule plainly: "The main thread can only process one task at a time. Any task that takes longer than 50 milliseconds is a long task."

    The test page had one. Halfway through its two seconds of frames, a timer started a task that kept the main thread busy for 300 ms, like a slow click handler. Chrome recorded it:

    The long taskPop out in the code editor (opens in a new tab)JavaScript
    // The test page's long task, as Chrome recorded it. Times are in ms.const task = { start: 1511.3, duration: 300 }; console.log(`The task started at ${task.start} ms.`);console.log(`It ended at ${task.start + task.duration} ms.`);
    • Line 2 holds when the task started, and how long it ran.
    • Line 4 prints The task started at 1511.3 ms. Line 5 prints It ended at 1811.3 ms.

    Now line these times up with the frame from the start of the lesson:

    1. At 1511.3 ms, the long task starts.
    2. At 1519.5 ms, 8.2 ms later, a rendering opportunity comes. The frame's update-the-rendering task has to wait its turn.
    3. At 1811.3 ms, the long task ends. The frame's callback runs at that same moment.

    The frame wasn't lost. It waited behind the long task, like the waiter's ticket behind the cook's big order. And the frames that should have come during the task? Look at when the next frame began:

    The frames that never camePop out in the code editor (opens in a new tab)JavaScript
    // When the frames on either side of the long task began, in ms.const before = 1519.5;const after = 1802.8; const gap = after - before;console.log(`The gap between them: ${gap.toFixed(1)} ms.`);console.log(`${Math.round(gap / 16.7) - 1} frames never came.`);
    • Lines 2 and 3 hold when the frames on either side of the task began.
    • Line 5 works out the gap. Line 6 prints The gap between them: 283.3 ms.
    • Line 7 divides the gap by one frame, 16.7 ms. That makes about 17 frames. The 17th is the frame at 1802.8 ms itself, so it takes 1 away. It prints 16 frames never came.

    283.3 ms is 17 beats of a 60 Hz screen, almost exactly. The frames stayed on the screen's beat. The 16 beats in between got no frame at all:

    Frames around a 300 ms task. Small cards are frames, one for each beat of a 60 Hz screen. Before the task, a frame comes on every beat. The frame that began at 1519.5 ms, just after the task started, is marked late. The next 16 beats are dashed cards: no frame came. Below, one wide card is the task, from 1511.3 ms to 1811.3 ms. Frames come again from 1802.8 ms.late16 frames never cameone task, 300 ms1511.3 ms1811.3 ms
    Each small card is one beat of a 60 Hz screen. During the 300 ms task, 16 beats got no frame (the dashed cards), and the frame that began just after the task started ran late.
    Real-life analogyA flip book with missing pages

    Draw a tea leaf in the corner of every page of a notebook, a little further along each time. Flip the pages, and the leaf drifts smoothly. Now tear out 16 pages from the middle. Flip again: the leaf stops, then jumps ahead.

    In real life: One page of the flip book
    In the browser: One frame
    In real life: Flipping at a steady speed
    In the browser: The screen's refresh rate
    In real life: The pages torn out
    In the browser: The 16 frames the long task skipped
    In real life: The leaf stopping, then jumping
    In the browser: Jank

    That's what Asha saw. If the leaf's script places it by the time, as MDN advises, it jumps to where it should be by now. Either way, it stops first.

    Taps wait too. A click handler runs in a task. If Asha taps during the long task, her tap's task waits in the queue behind it. The compositor thread can still scroll the page meanwhile, as lesson 6.1 showed, because scrolling doesn't need the main thread. But the leaf is moved by a script, on the main thread, so it waits.

    You can find long tasks in your own page. The Long Tasks API reports them as they happen. On the test page, a PerformanceObserver watching for longtask entries reported the task, 300 ms long.

    Why it matters: a frame can only be drawn between tasks. So keep tasks short. web.dev: "you can break up a long task into several smaller ones." Then the loop gets a turn between them, frames keep coming, and taps get answered. The Responsiveness: INP stage shows how.

    Task, microtask, or frame?

    Every callback in this lesson waits in one of three places. Read each row from left to right: what you write, what it queues, and when it runs.

    Where each callback waits
    When you writeWhat it queuesWhen it runs
    setTimeout(fn, 0)It queues a task when the delay is up.It runs when the loop picks that task, after the current task and its microtasks.
    A click handlerThe click queues a task that runs it.It runs when the loop picks the click's task.
    promise.then(fn) or awaitIt queues a microtask.It runs as soon as the current task ends, before the next task.
    queueMicrotask(fn)It queues a microtask.It runs with the other microtasks, before the next task.
    requestAnimationFrame(fn)It books a frame callback for the next frame.It runs at the start of the next frame, before style, layout and paint.

    Now sort some callbacks yourself:

    Task, microtask, or frame?
    • A setTimeout() callback
    • A click handler
    • Running a <script> as the page loads
    • A promise's .then() callback
    • The code after an await
    • A queueMicrotask() callback
    • A requestAnimationFrame() callback
    • Working out the layout after the frame callbacks
    Try it yourself
    0 of 8 correct

    Sort each callback by when the event loop runs it.

    Choose a category for every card. You can change an answer at any time; Reset clears them all.

    Common mix-ups

    • "setTimeout(fn, 0) runs right away." No. It queues a task. That task waits until the current task and all its microtasks are done.
    • "A promise callback is a task, like a timer." No. It's a microtask. It ran before the timer in every recorded run.
    • "requestAnimationFrame runs every 16.7 ms." No. It follows the screen. On a 120 Hz screen, frames come every 8.3 ms.
    • "The browser draws a frame after every task." No. It draws at rendering opportunities, at most once per refresh. Many tasks can run between two frames.
    • "A long task stops for a moment to let a frame through." No. A task always runs to its end. Updating the rendering is a task too, so it waits. During the 300 ms task, 16 frames never came.
    • "A long task only slows itself down." No. Every frame and every tap behind it waits too.
    • "Microtasks are too small to freeze a page." No. The loop runs microtasks until none are left. Microtasks that keep adding microtasks can keep it busy endlessly.

    Practice exercises

    Answer first, then use the hints.

    Exercise 1 · Warm-upA microtask from a timer

    This time, the timer's callback queues a microtask of its own. What does the last line printed say?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    setTimeout(() => {
      console.log("The timer ran.");
      queueMicrotask(() => console.log("The timer's microtask ran."));
    }, 0);
    console.log("The script finished.");

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 2 · Warm-upA 90 Hz screen

      The list now has 90 Hz where 75 Hz was. What does the second line say?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      for (const hz of [60, 90, 120, 144]) {
        console.log(`At ${hz} Hz, a frame lasts ${(1000 / hz).toFixed(1)} ms.`);
      }

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 3 · PracticeTwo timers and a microtask

        Two timers this time, and the first one queues a microtask. What does the third line printed say?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        setTimeout(() => {
          console.log("The first timer ran.");
          queueMicrotask(() => console.log("The first timer's microtask ran."));
        }, 0);
        setTimeout(() => console.log("The second timer ran."), 0);
        console.log("The script finished.");

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 4 · PracticeA leaf on a faster screen

          On a 60 Hz screen, this code moves the leaf 2 pixels each frame: 120 pixels a second. How many pixels a second does it move on a 120 Hz screen?

          Starter codeJavaScript
          const leaf = document.querySelector("#leaf");
          let x = 0;
          function moveLeaf() {
            x += 2;
            leaf.style.transform = `translateX(${x}px)`;
            requestAnimationFrame(moveLeaf);
          }
          requestAnimationFrame(moveLeaf);

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 5 · ChallengeThe frame callbacks in DevTools

            Download the grid page's trace and open it in DevTools' Performance panel. On the Main track, find one of the short tasks that repeat about every 16.7 ms. This lesson calls the first step inside it the frame callbacks. What does DevTools call it?

            Answer, then press Check. Spacing and letter case don’t matter.

              Exercise 6 · ChallengeFrames in a background tab

              What happens to requestAnimationFrame() callbacks when the page is in a background tab? Look it up on MDN.

              Answer, then press Check. Spacing and letter case don’t matter.

                Check your understanding

                For each question, ask: is this a task, a microtask, or part of drawing a frame? And what is it waiting for?

                The event loop quiz · 8 questionsScore: first tries count
                1. Question 1 of 8What does the microtask chain print last?

                  Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                  setTimeout(() => console.log("The timer ran."), 0);
                  queueMicrotask(() => {
                    console.log("The first microtask ran.");
                    queueMicrotask(() => console.log("The microtask it added ran too."));
                  });
                  console.log("The script finished.");

                  Choose an answer to see the explanation.

                2. Question 2 of 8What does the event loop do right after each task?

                  Choose an answer to see the explanation.

                3. Question 3 of 8How does drawing a new frame fit into the event loop?

                  Choose an answer to see the explanation.

                4. Question 4 of 8At 60 Hz, how often can the browser draw a frame at most?

                  Choose an answer to see the explanation.

                5. Question 5 of 8When do requestAnimationFrame callbacks run?

                  Choose an answer to see the explanation.

                6. Question 6 of 8Why move an animation in a requestAnimationFrame callback rather than with setTimeout(fn, 16)?

                  Choose an answer to see the explanation.

                7. Question 7 of 8What does the last line of the missing-frames example print?

                  Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                  // When the frames on either side of the long task began, in ms.
                  const before = 1519.5;
                  const after = 1802.8;
                  
                  const gap = after - before;
                  console.log(`The gap between them: ${gap.toFixed(1)} ms.`);
                  console.log(`${Math.round(gap / 16.7) - 1} frames never came.`);

                  Choose an answer to see the explanation.

                8. Question 8 of 8In the recording, why did the timer run before the frame callback every time?

                  Choose an answer to see the explanation.

                Key takeaways

                • The main thread runs one task at a time: a script, a timer's callback, a click handler.
                • After each task, the loop runs every microtask, even ones added on the way. Promise callbacks and awaits are microtasks.
                • The screen sets the beat. At 60 Hz, a rendering opportunity comes at most every 16.7 ms. At 120 Hz, every 8.3 ms.
                • Drawing a frame is a task too. Its frame callbacks run first, then style, layout and paint.
                • At 60 Hz, keep your work for each frame inside about 10 ms.
                • A long task holds up every frame and every tap. During a 300 ms task, 16 frames never came.

                Remember the one-liner.
                The browser draws only between tasks, at most once a refresh: keep tasks short, and frames keep coming.

                Sources

                The facts on this page come from these specs, docs and source files:

                Coming next: The pixel pipeline, also in How the browser loads and renders. Style, layout, paint and composite, and which changes trigger which steps.

                CompleteFrontend Clear concepts. Working examples.