completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

Long Tasks and Long Animation Frames

Find what blocks the main thread in the field, down to the script that caused it. A shop with its own script, two third-party scripts and two iframes, loaded and clicked in Chrome, with every longtask and long-animation-frame entry it got.

By the end, you can
  • 01
    Read a long task, and see its limitSay how long the main thread was stuck, and in which window or frame, and why the entry never names a script.
  • 02
    Read a long animation frameSay where a frame starts and ends, where its time went, and what each script in it tells you.
  • 03
    Explain a slow clickFind the frames that overlap a slow interaction, and name the script that kept it waiting.

Who blocked the main thread?

Asha is on the Leaf and Kettle page again. A small chat widget sits in the corner. She taps Add to cart for a second mint tea. For a moment, nothing happens. Then the cart updates.

In Event Timing and INP, you learned to time a moment like this. Here is what Event Timing wrote down for her press:

How long Asha's press waitedPop out in the code editor (opens in a new tab)JavaScript
// Asha presses Add to cart a second time.// Event Timing (lesson 11.4) wrote down these times, in milliseconds (ms).const press = { startTime: 2848.1, processingStart: 2895.5 }; console.log(`She pressed at ${press.startTime} ms.`);console.log(`The page started handling her press at ${press.processingStart} ms.`);console.log(`Her press waited ${(press.processingStart - press.startTime).toFixed(1)} ms.`);
  • Lines 1 and 2 are comments. Times are in milliseconds (ms), counted from when the page started loading.
  • Line 3 holds two times from Event Timing. startTime is when she pressed. processingStart is when the shop's code for her press began.
  • Line 5 prints She pressed at 2848.1 ms.
  • Line 6 prints The page started handling her press at 2895.5 ms.
  • Line 7 prints Her press waited 47.4 ms.

Lesson 11.4 called that wait the input delay. For 47.4 ms, something else had the main thread. But what? Event Timing can't say. It only times her interaction.

Remember the main thread from The event loop and frames. It runs the page's JavaScript, styles, layout and paint, one task at a time. So the real question is: which task was running when Asha pressed? Two browser APIs try to answer it:

  • The Long Tasks API reports each task that keeps the main thread busy for 50 ms or more.
  • The Long Animation Frames API reports each stretch of 50 ms or more when the main thread was too busy to update the screen. It also lists the scripts that ran in it.

To compare the two, the course built a small copy of the shop. Five pieces of work each keep the main thread busy for a set time:

  • The shop's own script, app.js, works for 80 ms as it loads. Its Add to cart handler works for 90 ms. Then it adds 400 items to a list, and reads how tall the list is.
  • A chat widget, chat.js, comes from another site. Its timer works for 70 ms.
  • An analytics script, analytics.js, also comes from another site. When its fetch() gets an answer, its callback works for 60 ms.
  • The shop's own ad, in an iframe, works for 60 ms as it loads.
  • A partner's iframe, from another site, works for 60 ms as it loads.

Chrome loaded the page with observers watching for both kinds of entry. Then Playwright, a tool that drives a browser from code, pressed Add to cart twice. The second time, it started chat's timer first and pressed a moment later. That's Asha's moment. Here it is, as each API saw it:

Asha's press while chat worked: 144 ms from 2,848.1 ms. Long tasks: self in the window, 70 ms; self in the window, 94 ms. Long animation frames: 70.1 ms with TimerHandler:setTimeout from chat.js; 94.2 ms with BUTTON#cart.onclick from app.js.Interactionlongtasklong-animation-frameclick, 144 msself, windowself, windowchat.js timerapp.js BUTTON#cartclick at 2848.1 ms
Asha's press, seen three ways. The long tasks only say "self, window". The frames name chat.js's timer, then the shop's click handler.

Read the picture from the top. The first row is Asha's whole interaction, 144 ms long. The middle row is the Long Tasks API: two tasks, each "self, window". That only means "something in this page". The bottom row is the Long Animation Frames API: first chat.js's timer, then the shop's click handler. So chat.js was in the way.

Real-life analogyThe cook's note, and the receipt

After a busy evening, the cook leaves the manager a note: "Busy for 70 minutes." It's true, but the manager can't act on it. Busy with what, and for whom? The till printed a receipt for the same 70 minutes. It lists every dish made, who ordered it, and how long each one took.

In real life: The note: "busy for 70 minutes"
In JavaScript: A longtask entry: how long, and in which window or frame
In real life: The receipt for the same 70 minutes
In JavaScript: A long-animation-frame entry
In real life: One line on the receipt: the dish, who ordered it, how long it took
In JavaScript: One script in the frame's scripts list
In real life: The time from the first order to the last plate
In JavaScript: The frame's duration
The main idea

Both APIs tell you the main thread was busy for 50 ms or more. A long task only says where: this page, or one of its frames. A long animation frame also says what: which scripts ran, and how each one started. Firefox and Safari write neither, so the entries in this lesson come from Chrome.

The rest of the lesson takes the two APIs apart. Long tasks come first, then frames and the scripts in them. Last come how long a frame blocks input, and how to link frames to a slow interaction like Asha's press.

The Long Tasks API and its limits

Start with the older API. In lesson 6.2, the main thread was a cook with a rail of order tickets. She takes the oldest ticket and finishes it before she takes the next. A long task is a ticket that keeps her busy for 50 ms or more. Every ticket behind it waits, including a tap. The JavaScript lesson Keeping the main thread responsive shows this with small programs.

Why 50 ms? The Long Tasks spec works it out from a goal in the RAIL performance model: answer input in under 100 ms. If no task takes 50 ms or more, then:

  1. A tap that arrives in the middle of a task waits less than 50 ms for that task to end.
  2. The tap's own task then takes less than 50 ms too.
  3. So the page answers in under 100 ms.

Here is the first long task Chrome wrote while the shop loaded, with its main fields:

One long task entryPop out in the code editor (opens in a new tab)JavaScript
// Chrome's first longtask entry while the shop loaded. Times in ms.const task = {  name: "self",  startTime: 55,  duration: 80,  attribution: [{ name: "unknown", containerType: "window" }],}; console.log(`Something ran for ${task.duration} ms, from ${task.startTime} ms.`);console.log(`Where: ${task.name}, in the ${task.attribution[0].containerType}.`);console.log(`What: ${task.attribution[0].name}.`);
  • Lines 2 to 7 hold the entry. startTime and duration say when the task began and how long it ran.
  • Line 3, name, says whose task it was. self means this page's own window.
  • Line 6, attribution, is a list with one item. Its containerType says what the work ran in: the window itself, or an iframe.
  • Lines 9 to 11 print Something ran for 80 ms, from 55 ms., Where: self, in the window. and What: unknown.

That last line is the whole problem. The spec says the attribution's name "will always return 'unknown'". It's the cook's note from the overview: busy for 80 ms, in this kitchen. Nothing about the order.

Here are all three long tasks Chrome reported while the shop loaded:

Three long tasks while the shop loadedPop out in the code editor (opens in a new tab)JavaScript
// Chrome's longtask entries while the shop loaded: [name, startTime, duration, containerType, containerSrc], in ms.const entries = [  ["self", 55, 80, "window", ""],  ["self", 135.9, 60, "window", ""],  ["same-origin-descendant", 210.9, 60, "iframe", "/ad.html"],]; for (const [name, startTime, duration, containerType, containerSrc] of entries) {  console.log(`${duration} ms at ${startTime}: ${name}, in the ${containerType}${containerSrc ? ` ${containerSrc}` : ""}`);}
  • Lines 2 to 6 hold the three entries. Each row is the name, the start time, the duration, the container type and the container's src.
  • Lines 8 to 10 print one line per entry.
  • The first two lines are 80 ms at 55: self, in the window and 60 ms at 135.9: self, in the window.
  • The third is 60 ms at 210.9: same-origin-descendant, in the iframe /ad.html.

You know what ran, because the course built the page:

  • The first task was app.js, as it loaded.
  • The second was the analytics callback.
  • The third was the shop's ad. same-origin-descendant means a frame inside this page, from the same origin. /ad.html is that iframe's src attribute.

But the entries don't say any of that. The first two look alike, though one was the shop's own code and the other a third party's. Chrome's guide puts it plainly: the API "at best only tells you the container the long task happened in", but "not the script or function which called it."

And one piece of work is missing completely. The partner's iframe, from another site, worked for 60 ms as it loaded. Chrome wrote no long task for it at all.

Why it matters: long tasks can tell you how often your page gets stuck, and for how long. They can't tell you whose code to fix. For that, you need the receipt.

Long Animation Frames

Asha doesn't see tasks. She sees the screen. After she taps, she waits for the screen to change. So the newer API measures something closer to what she feels: how long the screen waited for its next picture.

First, a reminder from lesson 6.2. A frame is one picture on the screen. Drawing a new one is a task too, called update the rendering. It waits its turn on the rail, like any other ticket.

The Long Animation Frames spec counts a long animation frame as one of two things, when it lasts 50 ms or more:

  1. A task after which nothing needs to be drawn. The frame is just that task.
  2. A task after which something does need to be drawn. The frame lasts "up until rendering is updated": through any tasks that run first, until the browser draws.

The frame starts when the main thread picks up work after being idle. Chrome's guide shortens the API's name to LoAF.

Real-life analogyThe waiter at the kitchen hatch

In lesson 6.2, a waiter came by the kitchen hatch on a steady round, ready to carry plates out. Say he arrives just as the cook picks up an order. She finishes it, picks up the next ticket on the rail, and only then plates up both. He stood at the hatch the whole time, with nothing to carry. If her job needs no plate, like refilling the sugar jars, he doesn't wait for it.

In real life: The waiter at the hatch, ready to carry plates out
In JavaScript: The screen, ready for its next picture
In real life: The cook picks up an order after a quiet moment
In JavaScript: The frame's startTime: work starts on an idle main thread
In real life: The plates finally go out
In JavaScript: The drawing at the end of the frame
In real life: 50 ms or more at the hatch with nothing to carry
In JavaScript: A long animation frame
In real life: A job with no plate, like refilling the sugar jars
In JavaScript: A task that changes nothing on screen: a frame on its own

You ask for these entries the same way as in The performance timeline, with an observer. That's Ravi, who taps you on the shoulder each time a new entry is written:

Watch for long animation framesJavaScript
if (PerformanceObserver.supportedEntryTypes.includes("long-animation-frame")) {  const observer = new PerformanceObserver((list) => {    for (const frame of list.getEntries()) console.log(`A ${frame.duration} ms frame.`);  });  observer.observe({ type: "long-animation-frame", buffered: true });}
  • Line 1 checks that this browser writes the type at all, with supportedEntryTypes from lesson 11.1.
  • Lines 2 to 4 make the observer. For each new frame, line 3 prints its duration.
  • Line 5 starts watching. buffered: true also brings the frames from before this line ran.

The shop ran an observer like this, for longtask entries too, in its very first script.

One frame, or two?

Here is where frames and tasks part ways. The shop has two timers the course could start at any time:

The shop's two timersJavaScript
window.twoTimers = (visual) => {  setTimeout(function first() {    busy(55);    if (visual) document.querySelector("#status").textContent = "first";  }, 0);  setTimeout(function second() {    busy(65);    if (visual) document.querySelector("#status").textContent = "second";  }, 0);};
  • Line 1 makes a function with one setting, visual: true or false.
  • Lines 2 and 6 start two timers. setTimeout(…, 0) puts each function on the rail as a task of its own.
  • Line 3 calls busy(55). busy is a small helper in the shop's code. It keeps the main thread busy for that many milliseconds. Line 7 does the same for 65 ms.
  • Lines 4 and 8 change the text on the page, but only when visual is true. Changed text is something the screen has to show.

The course ran it twice. The first time, visual was false, so nothing on the page changed. Chrome reported:

Two timers that change nothingPop out in the code editor (opens in a new tab)JavaScript
// Chrome's long animation frames when the timers changed nothing. Times in ms.const frames = [  { startTime: 3795.1, duration: 55, renderStart: 0 },  { startTime: 3850.2, duration: 65.1, renderStart: 0 },]; console.log(`Chrome reported ${frames.length} frames.`);for (const frame of frames) {  console.log(`One lasted ${frame.duration} ms. ${frame.renderStart === 0 ? "It drew nothing." : "Then it drew."}`);}
  • Lines 3 and 4 hold the two frames. renderStart is when the frame started drawing. It's 0 when the frame drew nothing.
  • Line 7 prints Chrome reported 2 frames.
  • Lines 8 to 10 print One lasted 55 ms. It drew nothing. and One lasted 65.1 ms. It drew nothing.

Each timer was a frame of its own. Neither had anything to draw, so each frame ended with its task. That's the first kind in the spec's list.

The second time, visual was true, so each timer changed the page:

Two timers that each change the pagePop out in the code editor (opens in a new tab)JavaScript
// Chrome's long animation frame when each timer changed the page. Times in ms.const frame = { startTime: 4599.5, duration: 120.8, renderStart: 4719.7 };const ran = ["first()", "second()"]; console.log(`It was one frame, ${frame.duration} ms long.`);console.log(`It ran ${ran.join(" and ")}, then started drawing at ${frame.renderStart} ms.`);
  • Line 2 holds the frame's times. Line 3 lists the two functions its scripts ran.
  • Line 5 prints It was one frame, 120.8 ms long.
  • Line 6 prints It ran first() and second(), then started drawing at 4719.7 ms.

Same two tasks, but now one frame. The first timer changed the page, so its frame had to last until the browser drew. Before drawing, the browser ran the second timer, which was already waiting on the rail. Chrome's guide says this is on purpose: the browser "may choose to tackle a queue of tasks over render updates", because drawing once for several tasks is less work. Here are both runs side by side:

Nothing changes on the page: long tasks of 55 ms and 65 ms; 2 long animation frames of 55 ms and 65.1 ms. Each timer changes the page: long tasks of 55 ms and 65 ms; one long animation frame of 120.8 ms.Nothing changes on the pagelongtask55 ms65 mslong-animation-frame55 ms, drew nothing65.1 ms, drew nothingEach timer changes the pagelongtask55 ms65 mslong-animation-frameone frame: 120.8 ms, then it drew
The same two timers, run twice. Long Tasks saw two tasks both times. The frames split only when nothing had to be drawn; when each timer changed the page, one frame held both, up to the drawing.

Long Tasks saw the same two tasks both times, because it counts tasks. Only the frames show that the second time, the screen went 120.8 ms without a new picture.

Why it matters: a slow screen isn't always one slow task. Chrome's guide points out that a frame can be made of several tasks under 50 ms each, which "collectively still block the browser's ability to render." Long Tasks would report none of them. Long animation frames catch them.

Where the frame's time went

A frame entry also says how its time was split. Two more times mark the drawing at its end:

  • renderStart: when the rendering began. Chrome's guide says the rendering includes requestAnimationFrame callbacks, style and layout, and resize and intersection observer callbacks.
  • styleAndLayoutStart: when style and layout began, near the end of the rendering.

You met style and layout in The pixel pipeline. Style works out which CSS rules apply to each element. Layout works out how much space each element takes, and where it goes.

Real-life analogyClearing the counter before the plates go out

When the cooking is done, the plates can't go out at once. First the cook arranges the counter: which dish goes on which plate, and where each plate stands. Only then does the waiter take them. On a quiet night this takes a moment. After a big new order, it takes longer.

In real life: Cooking the orders
In JavaScript: The work before rendering, from startTime to renderStart
In real life: Starting to plate up
In JavaScript: renderStart
In real life: Arranging the counter: which dish on which plate, and where each plate stands
In JavaScript: Style and layout, from styleAndLayoutStart
In real life: The plates going out
In JavaScript: The end of the frame

Here is the 120.8 ms frame, split the way Chrome's guide splits a frame:

Where the 120.8 ms wentPop out in the code editor (opens in a new tab)JavaScript
// The same frame's times, in ms.const frame = { startTime: 4599.5, duration: 120.8, renderStart: 4719.7, styleAndLayoutStart: 4719.7 };const end = frame.startTime + frame.duration; console.log(`Work before rendering: ${(frame.renderStart - frame.startTime).toFixed(1)} ms.`);console.log(`Rendering: ${(end - frame.renderStart).toFixed(1)} ms.`);console.log(`Of that, style and layout: ${(end - frame.styleAndLayoutStart).toFixed(1)} ms.`);
  • Line 2 holds the frame's four times. Line 3 works out when the frame ended.
  • Line 5 prints Work before rendering: 120.2 ms. That's mostly the two timers.
  • Line 6 prints Rendering: 0.6 ms. It counts from renderStart to the end of the frame.
  • Line 7 prints Of that, style and layout: 0.6 ms. Here renderStart and styleAndLayoutStart are the same time, so the rendering spent no time before style and layout.

Why it matters: almost all of this frame was script work, and almost none was drawing. So the fix is in the scripts, not in the CSS. The next section shows how to find which scripts.

Script attribution

Each long animation frame has a scripts list. Those are the receipt's lines.

A script here means one piece of JavaScript the browser started: a script file as it loads, an event handler, or a timer's callback. Only scripts that ran for more than 5 ms get a line.

Real-life analogyEach receipt line names the order

A good receipt line says more than the dish. "Table 4 rang the bell. Mint tea, from the house recipe book, page 256." Now you know whose order it was, and where to find its recipe. Another line on the same receipt is vaguer: "An order from next door, on a timer." That guest wouldn't give a name.

In real life: How the order came in: the bell at table 4
In JavaScript: invokerType and invoker: event-listener, BUTTON#cart.onclick
In real life: The recipe book it came from
In JavaScript: sourceURL: the script's file
In real life: The dish's name
In JavaScript: sourceFunctionName: addToCart
In real life: Where the recipe sits in the book
In JavaScript: sourceCharPosition: where in the file the function was defined
In real life: A guest from next door who won't give a name
In JavaScript: A script from another site, loaded without CORS

Here is a full line, from the frame of the first Add to cart press:

One script entry, with every detailPop out in the code editor (opens in a new tab)JavaScript
// One script entry from the first Add to cart click's frame. Times in ms.const script = {  invokerType: "event-listener",  invoker: "BUTTON#cart.onclick",  sourceURL: "http://127.0.0.1:4311/app.js",  sourceFunctionName: "addToCart",  sourceCharPosition: 256,  duration: 96.2,}; console.log(`${script.invoker} ran ${script.sourceFunctionName}() for ${script.duration} ms.`);console.log(`The function was defined in ${script.sourceURL}, at character ${script.sourceCharPosition}.`);
  • Lines 3 and 4 say how the script started. invokerType is the kind of start: event-listener, an event handler. invoker names it: the click handler on the button whose id is cart.
  • Lines 5 to 7 say where the code lives: the file, the function's name, and the character in the file where the function was defined.
  • Line 8 is how long the script ran.
  • Line 11 prints BUTTON#cart.onclick ran addToCart() for 96.2 ms.
  • Line 12 prints The function was defined in http://127.0.0.1:4311/app.js, at character 256.

Character 256 of app.js is the ( right after function addToCart. So the line takes you to the exact spot in the file.

Scripts start in different ways, and invokerType says which. The shop had four kinds. Read each row as a type, what starts it, and the shop's script of that type:

How each of the shop's scripts was started
invokerTypeWhat starts itThe shop's script of that type
classic-scriptA script file, running as the page loads it.app.js's start-up work. Its invoker is the file's own URL.
event-listenerA handler for an event, like a click.addToCart. Its invoker is BUTTON#cart.onclick.
user-callbackA function handed to a browser API, like setTimeout.chat.js's timer. Its invoker is TimerHandler:setTimeout.
resolve-promiseA then handler on a promise the browser made, like the one fetch() returns.The analytics callback. Its invoker is Window.fetch.then.

The spec has two more types. module-script is a JavaScript module running as it loads. reject-promise is like resolve-promise, but for a catch handler.

What a script from another site hides

Now the line that matters for Asha: chat's timer.

chat.js's entry, with its details hiddenPop out in the code editor (opens in a new tab)JavaScript
// The script entry for chat's timer. chat.js came from another site, without CORS.const script = {  invokerType: "user-callback",  invoker: "TimerHandler:setTimeout",  sourceURL: "http://localhost:4312/chat.js",  sourceFunctionName: "",  sourceCharPosition: -1,  duration: 70.1,}; console.log(`${script.invoker} ran code from ${script.sourceURL} for ${script.duration} ms.`);console.log(`Function name: "${script.sourceFunctionName}". Character: ${script.sourceCharPosition}.`);
  • Lines 3 and 4 say a timer started it.
  • Line 5 still names the file, chat.js.
  • Lines 6 and 7 hold nothing useful: an empty name, and -1 for the position. -1 means "not known".
  • Line 11 prints TimerHandler:setTimeout ran code from http://localhost:4312/chat.js for 70.1 ms.
  • Line 12 prints Function name: "". Character: -1.

The function in chat.js does have a name, pollChat. Chrome left it out on purpose. chat.js came from another site without CORS. CORS is a way for a server to say, with HTTP headers, which other sites may fully read its files. For a script loaded without it, the spec says "sourceFunctionName, sourceCharPosition, sourceLine, and sourceColumn are omitted." You get the URL, and that's all.

The analytics script also came from another site, but with CORS: its server sent an Access-Control-Allow-Origin header. Its entry kept a position, character 99, where analytics.js calls fetch(. For a promise, the spec records where the promise was made. So there's no function name, though the callback is called track.

Why it matters: a script from another site can hide its details from you. Chrome's guide explains how to get them back, in its list of common questions.

Frames inside the page

Each script entry says which window it ran in, in windowAttribution. The shop's own scripts got self. The ad's script got descendant: a frame inside this page. The partner's iframe got nothing at all. Chrome's guide: "Cross-origin iframes, web workers, service workers, and extension code running in an isolated world won't have script attribution in long animation frames".

What the script did inside

A script entry also has forcedStyleAndLayoutDuration: the time the script made the browser run style and layout early, in the middle of the script. That's lesson 6.3's cook, who stopped cooking to measure the plate. addToCart adds 400 items, then reads the list's height. To answer, the browser has to run style and layout right then. Its entry says 4.6 ms of its 96.2 ms went on that.

One warning: the line names where the script started, not the slow part inside it. Chrome's guide gives an example. A click handler calls a library, which calls a slow function. Then "the event handler will be reported in the LoAF entry, not the library nor the function." So start at the handler, and follow what it calls.

Blocking duration

A frame's duration says how long the screen waited. It doesn't say how long input waited. Those can be very different.

A tap that lands mid-task waits for the task to end: web.dev says "the browser cannot interrupt a task that's in progress." Between two tasks, though, Chrome's guide says the browser "will always try to prioritise inputs". So for input, what counts is how long each task runs, not the whole frame.

blockingDuration measures that. MDN describes it as "the total time in milliseconds for which the main thread was blocked from responding to high priority tasks, such as user input."

Real-life analogyLate fees at the library

The library lends each book for 50 days, free. After that, you pay for every extra day, book by book. You borrow one book for 55 days, then another for 65. Your fee is 5 days plus 15 days: 20 days. You had a library book at home for 120 days, but the fee only counts each book's days past its own limit.

In real life: Each book you borrow
In JavaScript: Each task in the frame
In real life: The first 50 days of each book, free
In JavaScript: The first 50 ms of each task
In real life: The days past that, which you pay for
In JavaScript: That task's blocking time
In real life: The total fee
In JavaScript: blockingDuration
In real life: How long you had a library book at home
In JavaScript: The frame's duration

Start with the two frames where the timers changed nothing. Each held one task:

Only the time past 50 ms countsPop out in the code editor (opens in a new tab)JavaScript
// The two frames when the timers changed nothing. Each held one task, in ms.const tasks = [55.1, 65.1]; for (const task of tasks) {  console.log(`A ${task} ms task blocks input for ${(task - 50).toFixed(1)} ms.`);}
  • Line 2 holds the two tasks' times, from each frame's one script.
  • Lines 4 to 6 take 50 from each. They print A 55.1 ms task blocks input for 5.1 ms. and A 65.1 ms task blocks input for 15.1 ms.

Chrome reported 5 and 15.1 ms. The rule uses the length of each task, which the entry doesn't show. Worked out from the scripts instead, the answer can be off by about 0.1 ms.

Now the frame where both timers changed the page. It has two tasks, and a short render at the end. The spec's rule has two steps:

  1. Add the render's time to the longest task.
  2. Add up each task's time past 50 ms.
The spec's rule, in codePop out in the code editor (opens in a new tab)JavaScript
// The spec's rule: add the render to the longest task, then count each task's time over 50 ms.function blockingDuration(tasks, render) {  const sorted = [...tasks].sort((a, b) => b - a);  sorted[0] += render;  return sorted.reduce((total, task) => total + Math.max(task - 50, 0), 0);} // The one frame when each timer changed the page: its two tasks and its render, in ms.console.log(`Blocking: ${blockingDuration([55, 65.1], 0.6).toFixed(1)} ms.`);
  • Lines 2 to 6 follow the spec. Line 3 sorts the tasks, longest first.
  • Line 4 adds the render to the longest task.
  • Line 5 adds up each task's time past 50 ms. A task under 50 ms adds 0.
  • Line 9 runs it on the frame: tasks of 55 and 65.1 ms, and a 0.6 ms render. It prints Blocking: 20.7 ms.

Chrome reported 20.8 ms, for a frame that lasted 120.8 ms. Here are both runs, with the part past 50 ms shaded:

Two timers of 55.1 ms and 65.1 ms. Changing nothing: 2 frames, blockingDuration 5 ms and 15.1 ms. Changing the page: 1 frame of 120.8 ms, blockingDuration 20.8 ms.Nothing changes50 ms50 ms+15.1Each changes the page50 ms50 ms+15.12 frames: blocking 5 ms + 15.1 ms1 frame of 120.8 ms: blocking 20.8 ms
The shaded part of each task is its time past 50 ms. That's all blockingDuration adds up, so one frame or two, the total comes out about the same.

One frame or two, the blocking came out about the same: 5 + 15.1 = 20.1 ms for the two frames, and 20.8 ms for the one. What mattered is that the work was split into two tasks. MDN makes the same point with its own example. Split work blocks less, "regardless of how the browser decides to handle the tasks".

Lighthouse's Total Blocking Time draws the same line. web.dev: "The blocking time of a given long task is its duration in excess of 50 milliseconds."

So which number should you look at? Chrome's guide gives a simple split:

  • For quick answers to taps, look at blockingDuration. Frames with a low one "should still feel responsive to input."
  • For smooth scrolling and animation, look at duration. A long frame still holds up what's drawn, even when input gets through.

Why it matters: to find what kept a tap waiting, sort your frames by blockingDuration, largest first, and read their scripts. When you fix them, split long tasks into shorter ones. The blocking drops even if the frame stays long.

Linking frames to interactions

Back to Asha's press. Event Timing gave it a start and a duration, and so does each frame. So you can match them up by time. Chrome's guide: "There is no direct API to link an INP entry with its related LoAF entry or entries". But you can do it "by comparing the start and end times of each."

It's like matching a late order to the kitchen's receipts by the time printed on each.

Two stretches of time overlap when each one starts before the other ends. Here is that test for Asha's press and chat's frame:

Does chat's frame overlap her press?Pop out in the code editor (opens in a new tab)JavaScript
// Asha's press and chat's frame, each as a start and an end, in ms.const press = { start: 2848.1, end: 2848.1 + 144 };const chatFrame = { start: 2825.2, end: 2825.2 + 70.1 }; const overlaps = chatFrame.start < press.end && chatFrame.end > press.start;console.log(`Chat's frame overlaps her press: ${overlaps}.`);
  • Line 2 turns her press into a start and an end. The end is her startTime plus the interaction's duration, 144 ms.
  • Line 3 does the same for chat's frame. It started at 2825.2 ms and lasted 70.1 ms.
  • Line 5 checks two things. Does the frame start before her press ends? Does it end after her press starts?
  • Line 6 prints Chat's frame overlaps her press: true.

Now run the same test on four frames from the recording:

The frames behind Asha's pressPop out in the code editor (opens in a new tab)JavaScript
// Asha's press (from Event Timing) and four frames around it, in ms.const press = { startTime: 2848.1, duration: 144 };const frames = [  { startTime: 1852.3, duration: 141.1, script: "BUTTON#cart.onclick from app.js" },  { startTime: 2825.2, duration: 70.1, script: "TimerHandler:setTimeout from chat.js" },  { startTime: 2895.3, duration: 94.2, script: "BUTTON#cart.onclick from app.js" },  { startTime: 3795.1, duration: 55, script: "TimerHandler:setTimeout from app.js" },]; const end = press.startTime + press.duration;const linked = frames.filter((frame) => frame.startTime < end && frame.startTime + frame.duration > press.startTime);console.log(`Frames that overlap her press: ${linked.length}.`);for (const frame of linked) console.log(`${frame.script}, in a ${frame.duration} ms frame.`);
  • Line 2 is her press. Lines 3 to 8 are four frames, each with the script that ran in it.
  • Line 10 works out when her press ended. Line 11 keeps the frames that overlap it.
  • Line 12 prints Frames that overlap her press: 2.
  • Line 13 prints one line for each frame it kept: TimerHandler:setTimeout from chat.js, in a 70.1 ms frame. and BUTTON#cart.onclick from app.js, in a 94.2 ms frame.

Put the times in order, and the story is complete:

  1. At 2825.2 ms, chat's timer started. It ran until 2895.3 ms.
  2. At 2848.1 ms, Asha pressed. Her press waited on the rail.
  3. At 2895.5 ms, the shop's code started handling her press. That's the 47.4 ms wait from the start of the lesson.
  4. The handler's frame ran addToCart, then drew the new cart.

Now the wait has a name: chat.js's timer. Event Timing said how long. Long animation frames said who.

The second frame has one more clue. Its firstUIEventTimestamp is the time of the first input it handled. It's 2848.1: Asha's press. But that time falls inside the first frame, chat's. Chrome's guide warns about this: the time "may be in a previous frame if there was a delay between the event happening and it being processed."

You don't have to write this matching yourself. The web-vitals library does it for INP. Its attribution build puts the frames that overlap the INP interaction in a list called longAnimationFrameEntries. The web-vitals library covers it.

One limit remains. In the recording, Firefox and WebKit listed neither entry type in PerformanceObserver.supportedEntryTypes. WebKit saw Asha's press wait 50 ms, through Event Timing. It just couldn't say why.

Here is the whole recording. Pick a moment to see its long tasks, its frames, and each frame's scripts:

Playground: long tasks and long frames
Main thread237.4 ms
longtasklong-animation-frame

the shop's scripts third-party scripts rendering

Each frame's scriptsblockingDuration
  1. Frame at 47.5 ms, 88.1 ms, rendering 0.2 ms30.3 ms
    • classic-script from app.js: 80 ms
  2. Frame at 135.9 ms, 60.1 ms, no rendering10.1 ms
    • Window.fetch.then from analytics.js: 60 ms
  3. Frame at 210.9 ms, 74 ms, rendering 0.2 ms10.7 ms
    • classic-script from ad.html: 60.1 ms (descendant)
Try it yourself
Page load: 3 long tasks, 3 long animation frames

The long tasks only say where the work ran: self (window) and same-origin-descendant (iframe /ad.html). The frames name what ran: 3 scripts. Their blockingDuration values add up to 51.1 ms.

Recorded in Chrome 156 on Apple M1 Pro. Firefox 155.0 and WebKit 26.6 write neither type of entry, so they reported nothing here.

Which API tells you?

Each fact below was either reported in the recording, or missing from it. Sort it by the API that reported it:

Which API tells you?
  • Some work kept the main thread busy for 50 ms or more
  • The work ran inside the shop's own ad frame
  • The URL of the script that ran
  • The name of the function in the shop's own script
  • When the browser started drawing
  • The name of the function inside chat.js, loaded without CORS
  • The work in the partner's frame on another site
Try it yourself
0 of 7 correct

Sort each fact by the API that reports it.

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

Common mix-ups

  • "A long task tells you which script ran." No. It names a window or a frame. Its attribution's name is always unknown.
  • "A long animation frame is one task." No. It can hold several. When the two timers changed the page, one 120.8 ms frame held both.
  • "A frame's duration is how long input was blocked." No. That frame lasted 120.8 ms, but its blockingDuration was 20.8 ms. Input can get in between tasks.
  • "Every script is listed, with its function name." No. Only scripts over 5 ms get a line. A script from another site, loaded without CORS, shows only its URL.
  • "The script entry points at the slow line." No. It points at where the script started, like the click handler, not at the slow function the handler called.
  • "These APIs work in every browser." No. Firefox and Safari write neither. Check supportedEntryTypes first.

Practice exercises

Answer first, then use the hints.

Exercise 1 · Warm-upThe example in Chrome's guide

Chrome's guide works the rule on two tasks of 55 and 65 ms, then a 20 ms render. What does this print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// The spec's rule: add the render to the longest task, then count each task's time over 50 ms.
function blockingDuration(tasks, render) {
  const sorted = [...tasks].sort((a, b) => b - a);
  sorted[0] += render;
  return sorted.reduce((total, task) => total + Math.max(task - 50, 0), 0);
}

// Chrome's guide: tasks of 55 and 65 ms, then a 20 ms render.
console.log(`Blocking: ${blockingDuration([55, 65], 20).toFixed(1)} ms.`);

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

    Exercise 2 · Warm-upA later press

    What if Asha had pressed at 2900 ms, just after chat's timer ended? What does the first line print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // Asha's press (from Event Timing) and four frames around it, in ms.
    const press = { startTime: 2900, duration: 144 };
    const frames = [
      { startTime: 1852.3, duration: 141.1, script: "BUTTON#cart.onclick from app.js" },
      { startTime: 2825.2, duration: 70.1, script: "TimerHandler:setTimeout from chat.js" },
      { startTime: 2895.3, duration: 94.2, script: "BUTTON#cart.onclick from app.js" },
      { startTime: 3795.1, duration: 55, script: "TimerHandler:setTimeout from app.js" },
    ];
    
    const end = press.startTime + press.duration;
    const linked = frames.filter((frame) => frame.startTime < end && frame.startTime + frame.duration > press.startTime);
    console.log(`Frames that overlap her press: ${linked.length}.`);
    for (const frame of linked) console.log(`${frame.script}, in a ${frame.duration} ms frame.`);

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

      Exercise 3 · PracticeThe worst frame while loading

      In the playground, pick Page load. Which of its frames had the largest blockingDuration? Give the number, in ms.

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

        Exercise 4 · PracticeThe first press's drawing

        In the playground, pick Click Add to cart. How long did its frame spend rendering, in ms?

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

          Exercise 5 · ChallengeGet chat.js's details back

          chat.js hid its function name and position. Chrome's guide says what to add to its script tag to get them back. What do you add?

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

            Exercise 6 · ChallengeA failed fetch

            Say analytics' fetch() had failed, and a catch handler ran instead of then. What would the script entry's invoker be?

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

              Check your understanding

              For each question, ask yourself which API reported it, where a frame starts and ends, and what a script entry can and can't show.

              Long tasks and long animation frames quiz · 7 questionsScore: first tries count
              1. Question 1 of 7If first() and second() had been one 120.1 ms task, with the same render, what would the blocking example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // The spec's rule: add the render to the longest task, then count each task's time over 50 ms.
                function blockingDuration(tasks, render) {
                  const sorted = [...tasks].sort((a, b) => b - a);
                  sorted[0] += render;
                  return sorted.reduce((total, task) => total + Math.max(task - 50, 0), 0);
                }
                
                // What if first() and second() had been one task of 120.1 ms, with the same render?
                console.log(`Blocking: ${blockingDuration([120.1], 0.6).toFixed(1)} ms.`);

                Choose an answer to see the explanation.

              2. Question 2 of 7What does the first line of the linking example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // Asha's press (from Event Timing) and four frames around it, in ms.
                const press = { startTime: 2848.1, duration: 144 };
                const frames = [
                  { startTime: 1852.3, duration: 141.1, script: "BUTTON#cart.onclick from app.js" },
                  { startTime: 2825.2, duration: 70.1, script: "TimerHandler:setTimeout from chat.js" },
                  { startTime: 2895.3, duration: 94.2, script: "BUTTON#cart.onclick from app.js" },
                  { startTime: 3795.1, duration: 55, script: "TimerHandler:setTimeout from app.js" },
                ];
                
                const end = press.startTime + press.duration;
                const linked = frames.filter((frame) => frame.startTime < end && frame.startTime + frame.duration > press.startTime);
                console.log(`Frames that overlap her press: ${linked.length}.`);
                for (const frame of linked) console.log(`${frame.script}, in a ${frame.duration} ms frame.`);

                Choose an answer to see the explanation.

              3. Question 3 of 7A long task entry says self, in the window. What doesn't it tell you?

                Choose an answer to see the explanation.

              4. Question 4 of 7chat.js came from another site, without CORS. What did its script entry leave out?

                Choose an answer to see the explanation.

              5. Question 5 of 7Two timers of 55 and 65 ms made two frames when they changed nothing, and one frame when each changed the page. Why?

                Choose an answer to see the explanation.

              6. Question 6 of 7A frame lasted 120.8 ms, but its blockingDuration was 20.8 ms. What does that tell you?

                Choose an answer to see the explanation.

              7. Question 7 of 7Firefox and Safari don't write these entry types. How should your code handle that?

                Choose an answer to see the explanation.

              Key takeaways

              • A long task is one task of 50 ms or more. Its entry says where it ran, never which script.
              • A long animation frame runs from the first task on an idle main thread to the next drawing, or to the end of a task with nothing to draw.
              • Each frame lists its scripts over 5 ms: how each one started, its file, and often its function. A script from another site without CORS shows only its URL.
              • blockingDuration counts each task's time past 50 ms, with the render added to the longest. Sort by it to find what kept input waiting.
              • Match frames to a slow interaction by their times. Firefox and Safari write neither entry type.

              Remember the one-liner.
              A long task is the cook's note; a long animation frame is the receipt that says who ordered what.

              Sources

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

              Coming next: User Timing and custom metrics. Measure what matters to your product, like the time to the hero image or to the first search result.

              CompleteFrontend Clear concepts. Working examples.