Long tasks
Why a task of 50 ms or more makes a tap wait, how Total Blocking Time adds up those waits, how DevTools marks long tasks, and what long frames add.
- 01Explain why long tasks hurt tapsWork out how long a tap waits when it lands in a task, and why the line is drawn at 50 ms.
- 02Read Total Blocking TimeAdd up the part of each long task past 50 ms between FCP and TTI, the way Lighthouse does.
- 03Find long tasks, and what they missSpot them in DevTools, and tell when a long animation frame shows a slow screen that no long task does.
Why did Asha's tap wait?
Asha opens the Leaf and Kettle tea list on her phone. The list appears. She taps Add on Mint tea. Nothing happens. More than a third of a second later, the cart says 1.
Lesson 26.3 found the reason. A script called reviews.js was running when she tapped. It reads this week's reviews as the page starts. Ravi builds the site, and he can't drop it. The shop needs its popular teas. But he can change how the script runs.
So his question is this. Does it matter if the same work runs in one piece or in many? And how can he tell, without Asha there to tap?
Start with Asha's tap. Here are three times from the course's recording of it:
// Asha's tap on the one-task page, from the course's recording. Times in ms since the page started.const task = { start: 1060.8, end: 1471.7 };const tap = 1152.2; console.log(`reviews.js's task ran for ${(task.end - task.start).toFixed(1)} ms.`);console.log(`Asha tapped ${(tap - task.start).toFixed(1)} ms into it.`);console.log(`Her tap waited ${(task.end - tap).toFixed(1)} ms for the rest.`);- Line 1 is a comment. Times are in milliseconds (ms) since the page started. A millisecond is a thousandth of a second.
- Line 2 holds when reviews.js's task started and ended. A task is one piece of work the main thread picks up and runs to the end. The main thread is the one thread that runs the page's JavaScript and draws its frames.
- Line 3 holds the moment Asha tapped.
- Lines 5 to 7 print
reviews.js's task ran for 410.9 ms.,Asha tapped 91.4 ms into it.andHer tap waited 319.5 ms for the rest.
Here is what Asha saw. These are real frames Chrome captured. A frame is one picture the browser draws on the screen.
The list had been on screen for a second. It looked ready. But the main thread was busy, and a browser can't stop a task halfway. web.dev puts it plainly: "the browser cannot interrupt a task that's in progress." So her tap waited for the rest of it. Why it matters: a page that looks ready isn't ready while a long task runs.
Meera starts copying a 400-page report on the office's one photocopier. Ravi walks up with a single page. The machine won't stop halfway, so he waits until her whole job is done. If Meera copies 50 pages at a time and steps aside between batches, Ravi waits for one batch at most.
- In real life: The office's one photocopier
- In the browser: The main thread
- In real life: Meera's 400-page report, copied in one go
- In the browser: reviews.js as one long task
- In real life: Ravi with a single page to copy
- In the browser: Asha's tap
- In real life: Copying 50 pages, then stepping aside
- In the browser: The same work as short tasks
A long task keeps the main thread busy for 50 ms or more. A tap that arrives during a task waits for the rest of it. Split the same work into short tasks, and each tap waits for one short task at most. The work doesn't shrink. The waits do.
Two lessons elsewhere on the site look at the same rule from other sides. The JavaScript lesson Keeping the main thread responsive shows it with small programs. The system design lesson Runtime performance shows how to plan a whole app around it. This lesson measures it.
The rest of the lesson answers Ravi's question in five steps:
- Why the line is drawn at 50 ms.
- How long a tap waits when it comes at a random moment.
- How Total Blocking Time adds those waits up, with no taps at all.
- How DevTools marks long tasks.
- What long animation frames see that long tasks miss.
How the course recorded it
Here is reviews.js, as the one-task page has it:
// reviews.js: reads this week's 4,000 reviews, 4,000 at a time.const BATCH = 4000;const about = document.querySelector("#about");let read = 0;for (let start = 0; start < 4000; start += BATCH) { setTimeout(function readReviews() { work(BATCH * 23_000); read += BATCH; about.textContent = "Reviews read: " + read + " of 4000"; }, 0);}- Line 2 sets
BATCH, how many reviews each task reads. Here it's 4,000: all of them, in one task. - Lines 5 to 11 queue one task per batch.
setTimeout(…, 0)runs a function as a new task, once the main thread is free. - Line 7 does the work: 23,000 steps for each review, with
work()from lesson 26.3. It takes the same steps on any computer. - Line 9 shows how many reviews have been read so far.
- Three pages: the same tea list, with
BATCHset to 4,000, 500 or 100. That makes one task, eight tasks or forty tasks. The work is the same each time: 92 million steps. - Asha's phone: this computer in Chrome 157, with its processor, the CPU, slowed 4 times, as in lessons 26.2 and 26.3. The server held reviews.js back 1 second, like a slow network, so the list appeared first.
- Loads: each page loaded three times with a trace, Chrome's detailed record of everything it did, with screenshots. The lesson quotes the middle load of each page.
- Taps: one tap on Add, Mint tea, at each of 24 moments, 25 ms apart, on each page. That's 72 more loads.
- Lighthouse: Lighthouse 13.6.0 loaded each page once, on the same Chrome, with the CPU slowed 4 times.
Why the line is at 50 ms
reviews.js's task ran for 410.9 ms. Is that long? The Long Tasks API, a browser feature that reports long tasks to the page, draws the line at 50 ms. Lesson 11.5 showed where that number comes from. Here it is again, as an everyday rule.
The Leaf and Kettle staff room has one microwave, with a sign: "Two minutes at most." When Ravi walks up, the person using it is done within two minutes. Then his own lunch takes two minutes at most. So everyone has hot food within four minutes.
- In real life: Hot lunch within four minutes
- In the browser: An answer to a tap in under 100 ms
- In real life: "Two minutes at most" for each person
- In the browser: No task of 50 ms or more
- In real life: Waiting for the person in front
- In the browser: Waiting for the task already running
- In real life: Heating your own lunch
- In the browser: Running the tap's own handler
The goal comes from RAIL, Google's older guide to speed. It says to answer a tap within 100 ms, so the result feels immediate. Lesson 2.1 met the same 0.1 second limit. The Long Tasks spec splits those 100 ms in two:
// The Long Tasks spec's sum: an answer in under 100 ms, in two halves. Times in ms.const goal = 100;const longestTask = 50;const handler = goal - longestTask; console.log(`Waiting for the task already running: up to ${longestTask} ms.`);console.log(`Running the tap's own handler: up to ${handler} ms.`);console.log(`With reviews.js's 410.9 ms task instead: up to ${410.9 + handler} ms.`);- Line 2 is the goal. Line 3 is the most a task may run.
- Line 4 leaves the rest for the tap's own handler: the code that runs when the tap arrives.
- Lines 6 and 7 print
Waiting for the task already running: up to 50 ms.andRunning the tap's own handler: up to 50 ms. - Line 8 prints
With reviews.js's 410.9 ms task instead: up to 460.9 ms.
The spec says it in one line: a page without long tasks "should respond to user input in under 100ms". Today, web.dev recommends Core Web Vitals over RAIL for setting goals. Their measure of taps, INP, calls 200 ms or less good. But the 50 ms line for a single task stayed. Why it matters: 50 ms isn't a target. It's the most one task may take if every tap is to feel instant.
A line, not a cliff
What about a task right at the line? On the eight-task page, each task read 500 reviews. That's about 50 ms of work on Asha's phone. Some tasks came out just over the line, some just under. Here is what Chrome's Long Tasks API reported in one load:
// One load of the eight-task page: the tasks Chrome's Long Tasks API reported, in ms.const reported = [58, 53, 53, 54, 51];const tasks = 8; console.log(`Long tasks reported: ${reported.length} of ${tasks}`);console.log(`Time past 50 ms: ${reported.map((ms) => ms - 50).join(" + ")} ms`);- Line 2 holds the five tasks it reported. Line 3 is how many tasks there were.
- Line 5 prints
Long tasks reported: 5 of 8. - Line 6 prints
Time past 50 ms: 8 + 3 + 3 + 4 + 1 ms.
A millisecond decided which tasks were reported. But a tap that landed in any of them waited about 50 ms at most. Why it matters: going over the line by a little costs a little. Total Blocking Time counts it the same way, as you'll see.
A tap at a random moment
Asha doesn't plan when to tap. Any moment is as likely as another. So the useful question is: if she taps at a random moment, how long does she wait?
Think of the photocopier again. If Ravi walks up during Meera's job, he waits for the rest of it. He might arrive near the start, or near the end. On average, he waits for half of it. And the longer her job, the more likely he walks into it at all.
Those two ideas make a small model. A tap lands in a task with a chance equal to the task's length divided by the whole stretch of time. If it lands, it waits half the task on average:
// A tap at a random moment in 600 ms of start-up. The same 400 ms of work, three ways. Times in ms.const pages = { one: [400], eight: Array(8).fill(50), forty: Array(40).fill(10) };const windowMs = 600; for (const [name, tasks] of Object.entries(pages)) { const average = tasks.reduce((sum, ms) => sum + (ms / windowMs) * (ms / 2), 0); console.log(`${name}: at most ${Math.max(...tasks)} ms, ${average.toFixed(1)} ms on average`);}- Line 2 holds the three pages: one task of 400 ms, eight of 50 ms, or forty of 10 ms. Line 3 is the 600 ms the taps come in.
- Line 6 adds up, for each task, its chance (
ms / windowMs) times its average wait (ms / 2). - Line 7 prints one line per page. The first is
one: at most 400 ms, 133.3 ms on average. Then comeeight: at most 50 ms, 16.7 ms on averageandforty: at most 10 ms, 3.3 ms on average.
Eight tasks cut the average wait by eight. Forty cut it by forty. The work didn't shrink. Each task did.
Now the recording. The course tapped Add at 24 moments, 25 ms apart, on each page. For each tap, the trace shows which task was running, and when it ended. Here is how long the taps waited for the main thread:
// 24 taps on each page, 25 ms apart. Each waited for the rest of the task it landed in. Times in ms.const taps = { one: { average: 129.4, longest: 368.3, over50: 15 }, eight: { average: 13.7, longest: 51.5, over50: 1 }, forty: { average: 3.8, longest: 11.9, over50: 0 },};for (const [name, t] of Object.entries(taps)) { console.log(`${name}: ${t.average} ms on average, ${t.longest} ms at most, ${t.over50} of 24 over 50 ms`);}- Lines 3 to 5 hold each page's average wait, its longest wait, and how many taps waited over 50 ms.
- Line 8 prints
one: 129.4 ms on average, 368.3 ms at most, 15 of 24 over 50 ms. Then comeseight: 13.7 ms on average, 51.5 ms at most, 1 of 24 over 50 ms. Last isforty: 3.8 ms on average, 11.9 ms at most, 0 of 24 over 50 ms.
The model said 133.3, 16.7 and 3.3 ms. The recording says 129.4, 13.7 and 3.8 ms. Why it matters: you don't need Asha to know how her taps would go. The lengths of the tasks tell you.
How the taps were timed
Chrome also timed each tap itself. On the one-task page, each tap that landed in the task waited the rest of it, plus 20 ms more for the typical tap. The extra came from the tool the course tapped with: Chrome's DevTools protocol, which lets a program drive the browser. Its source code says it makes "sure the compositor is up to date before sending a touch event". So it holds each touch until the page has drawn a frame.
On the eight- and forty-task pages, that hold got in the way. Every tap that came during the work reached the page only after all the work was done. A real finger isn't held. Chrome's scheduler, the part that picks the next task, gives input its highest priority. So a real tap runs as soon as the current task ends. That's why the waits above come from the recorded tasks: each tap waits for the rest of the task it landed in.
Pick a page to see all 24 taps: when each came, which task it landed in, and how long it waited.
76.8 msafter the work startedin task 1waited218.8 ms18.8 msafter the work startedin task 1waited368.3 ms42.2 msafter the work startedin task 1waited350.3 ms74.3 msafter the work startedin task 1waited323.5 ms92.2 msafter the work startedin task 1waited289.2 ms116.8 msafter the work startedin task 1waited289.6 ms142.2 msafter the work startedin task 1waited260 ms166.2 msafter the work startedin task 1waited193.8 ms192.4 msafter the work startedin task 1waited195.2 ms215 msafter the work startedin task 1waited170 ms264.9 msafter the work startedin task 1waited84.9 ms266 msafter the work startedin task 1waited127.5 ms288.3 msafter the work startedin task 1waited97.4 ms316.1 msafter the work startedin task 1waited69.6 ms341.8 msafter the work startedin task 1waited50.2 ms366.8 msafter the work startedin task 1waited17.4 ms391.4 msafter the work startedbetween or after the taskswaited0 ms417.7 msafter the work startedbetween or after the taskswaited0 ms442.7 msafter the work startedbetween or after the taskswaited0 ms468.5 msafter the work startedbetween or after the taskswaited0 ms492.2 msafter the work startedbetween or after the taskswaited0 ms518.8 msafter the work startedbetween or after the taskswaited0 ms542.7 msafter the work startedbetween or after the taskswaited0 ms568.9 msafter the work startedbetween or after the taskswaited0 ms
16 of 24 taps landed inside a task. 15 waited more than 50 ms. The rest came between tasks, or after the work, and didn't wait for any.
Total Blocking Time adds up the bad moments
A lab tool like Lighthouse can't tap like Asha. It loads the page, and nobody touches it. So how can it tell whether taps would wait? It uses Total Blocking Time, or TBT. Lesson 3.2 introduced it: the part of each long task past 50 ms, added up. Here is why that sum is the right one.
Take one long task. A tap that lands early in it waits more than 50 ms, because more than 50 ms of the task is still to come. A tap in its last 50 ms waits less. So the stretch of a task where a tap would wait too long is its length minus 50 ms. TBT adds up those stretches. It's the total time when a tap would have waited more than 50 ms.
A car park in town charges only from 8 in the morning to 6 at night. The first 50 minutes of each stay are free. Meera parks from 7:30 to 9:10. The meter starts at 8. It gives her 50 free minutes, then charges for the last 20.
- In real life: Each stay in the car park
- In Lighthouse: Each long task
- In real life: Charging hours, 8 in the morning to 6 at night
- In Lighthouse: From FCP to TTI
- In real life: The first 50 minutes of each stay, free
- In Lighthouse: The first 50 ms of each task
- In real life: The minutes Meera pays for
- In Lighthouse: That task's blocking time
- In real life: The day's paid minutes, for every car
- In Lighthouse: TBT
The charging hours are Lighthouse's window:
- It starts at FCP, the first contentful paint: when the first text or image appears. The code behind Lighthouse's rule says there's "little risk of user input happening before FCP". There's nothing to tap yet.
- It ends at TTI, Time to Interactive. web.dev explains that TTI waits until the main thread "has been free of long tasks for at least five seconds". Lighthouse's code also waits for the network to go quiet.
Lighthouse cuts each task to the window first, then takes off 50 ms. Here is its rule, shortened, on the one-task page:
// Lighthouse's rule, shortened: the part of each task past 50 ms, between FCP and TTI. Times in ms.function totalBlockingTime(tasks, fcp, tti) { let total = 0; for (const { start, duration } of tasks) { const from = Math.max(start, fcp); const to = Math.min(start + duration, tti); if (to - from > 50) total += to - from - 50; } return total;}// The one-task page: the page's first rendering, then reviews.js's task.const tasks = [{ start: 50.5, duration: 79.8 }, { start: 1051, duration: 411.7 }];console.log(`TBT: ${totalBlockingTime(tasks, 160, 1462.8).toFixed(1)} ms`);- Line 1 is a comment. Lines 2 to 10 are the rule.
- Line 5 moves a task's start up to FCP if it began earlier. Line 6 moves its end back to TTI if it ran later.
- Line 7 adds the part past 50 ms, if there is one.
- Line 12 holds two tasks from the recording. The first was the page drawing itself for the first time, before FCP. The second is reviews.js.
- Line 13 prints
TBT: 361.7 ms. The first task ended before FCP, so it adds nothing.
Is the port right? The course ran it on Lighthouse's own trace of each page, with Lighthouse's FCP and TTI. It gave Lighthouse's TBT every time, to the tenth of a millisecond. For the one-task page, Lighthouse reported 341.6 ms. Its load was a different load, and its task was a little shorter.
TBT and the random taps
TBT is the time when a tap would wait over 50 ms. So TBT divided by the whole stretch is the share of random taps that wait that long. Check it against the 24 taps, which covered 600 ms:
// TBT is the time when a tap would wait more than 50 ms. The 24 taps covered 600 ms.const windowMs = 600;const tbt = { one: 361.7, eight: 22.2, forty: 0 }; for (const [name, ms] of Object.entries(tbt)) { const share = ms / windowMs; console.log(`${name}: ${Math.round(share * 100)}% of moments, about ${(share * 24).toFixed(1)} of 24 taps`);}- Line 3 holds each page's TBT, from the course's loads.
- Line 6 divides it by the 600 ms. Line 7 prints the share, and how many of 24 taps that makes.
- It prints
one: 60% of moments, about 14.5 of 24 taps,eight: 4% of moments, about 0.9 of 24 tapsandforty: 0% of moments, about 0.0 of 24 taps.
The real taps said 15, 1 and 0. Why it matters: TBT isn't an abstract score. It's the part of the load when a tap would feel slow. Lighthouse rates up to 200 ms as fast on phones. The one-task page is far over. The eight-task page is well under.
- A
411.7 mstask at1,051 msadds361.7 ms
The course's load: TBT 361.7 ms, counted up to TTI at 1,462.8 ms.
Lighthouse's own load: FCP 165.1 ms, TBT 341.6 ms.
1 task of 50 ms or more between FCP and TTI. Each adds the part of it past 50 ms.
Long tasks in DevTools
Ravi wants to see these tasks himself. He records a load in DevTools' Performance panel, from lesson 7.1. The Main track shows every task the main thread ran, as gray bars named Task.
DevTools marks each task over 50 ms in two ways. Its docs say long tasks are "highlighted with a red triangle, and with the part over 50 milliseconds shaded in red". The shading is a pattern of red stripes. DevTools' own code starts the stripes 50 ms into the task.
A spell checker doesn't fix your writing. It puts a red line under the words to look at, so they stand out on a long page. You still read each one, and decide what to change.
- In real life: The red line under a word
- In DevTools: The red triangle on a task
- In real life: Only words that need a look get one
- In DevTools: Only tasks over 50 ms get one
- In real life: Clicking the word to see suggestions
- In DevTools: Clicking the task to see what ran inside
Here are the three pages, the way DevTools would draw them. The shaded ends are the stripes:
The one-task page has one big stripe. The eight-task page has thin slivers. The forty-task page has none. Add up the stripes, and you get each page's TBT.
To see what ran inside a long task, click it. web.dev suggests the Bottom-Up tab, grouped by activity, to see what took the most time. Lesson 7.2 read a trace this way. Why it matters: DevTools draws the same 50 ms line as Lighthouse. So the stripes show where TBT comes from, task by task.
Where long tasks and long animation frames disagree
The forty-task page has no long tasks, and a TBT of 0. Is it perfect? Look at what Asha saw while the reviews were read:
The line jumped from 900 to 1,900, then to 2,900 and 3,900. It never showed 100, 200 or 300. Each task changed it, but Chrome drew a new frame only after about ten tasks.
Lesson 11.5 met the API that sees this: Long Animation Frames, or LoAF. A long animation frame is a stretch of 50 ms or more, from when the main thread starts work until it draws the next frame. Here is what each API reported on the forty-task page:
// The forty-task page, while the reviews were read: what each API reported. Times in ms.const longTasks = [];const frames = [ { duration: 105.6, blockingDuration: 0, scripts: 9 }, { duration: 111.2, blockingDuration: 0, scripts: 10 }, { duration: 107.4, blockingDuration: 0, scripts: 10 }, { duration: 107.7, blockingDuration: 0, scripts: 10 },];console.log(`Long tasks: ${longTasks.length}`);console.log(`Long animation frames: ${frames.length}, up to ${Math.max(...frames.map((f) => f.duration))} ms`);console.log(`Blocking: ${frames.reduce((sum, f) => sum + f.blockingDuration, 0)} ms`);- Line 2: the Long Tasks API reported nothing.
- Lines 3 to 8: four long animation frames. Each lists how many scripts ran in it, and its blocking duration: how long it could have kept a tap waiting, from lesson 11.5.
- Lines 9 to 11 print
Long tasks: 0,Long animation frames: 4, up to 111.2 msandBlocking: 0 ms.
Meera keeps the café's specials board up to date between small chores. Each chore takes a minute. A customer with a question gets an answer between any two chores. But Meera only steps over to the board every ten chores. So the board is always ten minutes behind.
- In real life: Each one-minute chore
- In the browser: Each 10 ms task
- In real life: Answering a question between chores
- In the browser: A tap gets in between tasks: blocking duration 0
- In real life: Updating the board every ten chores
- In the browser: A new frame after about ten tasks
- In real life: A board that's always ten minutes behind
- In the browser: A long animation frame
Both APIs are right. They measure different things:
- The Long Tasks API times each task on its own. None reached 50 ms.
- Long Animation Frames time how long the screen waited for its next picture. Chrome's guide warns about this case. A frame "could be made up of several tasks smaller than this 50 millisecond limit, yet collectively still block the browser's ability to render."
The blocking duration is 0 because no task was long. A tap would have got in between tasks. But the screen still stood still for over 100 ms at a time. For a line of text, that's fine. For an animation, it would stutter. Why it matters: long tasks and TBT tell you about taps. Long animation frames also tell you about the screen.
Drawing is a task of its own
The two APIs also disagree about drawing. Chrome's guide explains that rendering happens "in separate tasks" from the script that changed the page. The page's first rendering shows this. On the one-task page, it ran as one task of 79.8 ms, mostly working out the layout. DevTools gave it a red triangle. Long Animation Frames reported it too, at the end of an 80.1 ms frame. But the page's Long Tasks observer got no entry for it. And TBT ignored it, because it came before FCP.
Why it matters: if your page is slow to draw, the Long Tasks API may not say so. Long animation frames will.
What each tool saw
Read this table across. Each row is one page, while its reviews were read, in the load the lesson quotes. The last column is the longest of the 24 taps, worked out from the tasks.
| Shape of the work | Long Tasks API | Total Blocking Time | Long animation frames | Longest wait for a tap |
|---|---|---|---|---|
| One task of 4,000 reviews. | 1 entry, up to 411 ms. | 361.7 ms. | 1 frame, up to 413.4 ms. | 368.3 ms. |
| Eight tasks of 500 reviews. | 5 entries, up to 58 ms. | 22.2 ms. | 4 frames, up to 110.1 ms. | 51.5 ms. |
| Forty tasks of 100 reviews. | No entries. | 0 ms. | 4 frames, up to 111.2 ms. | 11.9 ms. |
That's Ravi's answer. Forty tasks cut the longest wait from 368.3 ms to 11.9 ms, and TBT from 361.7 ms to 0. The screen still waited about 100 ms for each new frame. How to split work so taps and frames both get a turn is the next lesson's subject.
Try the three shapes on this page's own main thread, and see what your browser reports:
// What the panel runs. busy(ms) keeps the main thread working until the time is up.const busy = (ms) => { const end = performance.now() + ms; while (performance.now() < end) {} };for (let i = 0; i < tasks; i++) { setTimeout(() => { busy(ms); output.textContent = `Task ${i + 1} of ${tasks} done`; }, 0);}Nothing run yet.
Pick a shape, then press Run. Each shape is 200 ms of work on this page's main thread, and each task changes the line below.
Does it add to TBT?
Each task below is on a page Lighthouse is loading. Sort it by whether it adds to Total Blocking Time:
- A 120 ms task after the first paint, before TTI
- A 45 ms task after the first paint
- A 200 ms task that ends before the first paint
- A 90 ms task that starts 20 ms before the first paint
- Forty 10 ms tasks in a row
- An 80 ms layout task after the first paint
- A 300 ms task, 10 seconds after TTI
Sort each task by whether Lighthouse adds any of it to TBT.
Common mix-ups
- "A task under 50 ms doesn't make a tap wait." No. A tap waits for the rest of whatever task is running. Under 50 ms, that rest is just shorter.
- "Splitting the work makes it finish sooner." No. It's the same work. The forty tasks ran for 448.8 ms from the first to the last, against 411.7 ms for the one task.
- "TBT is how long taps waited." No. Nobody taps during a Lighthouse load. TBT is the time a tap would have waited more than 50 ms.
- "No long tasks means a smooth screen." No. The forty-task page had none, and four long animation frames of over 100 ms.
- "Every long task adds to TBT." No. Only the part past 50 ms, between FCP and TTI. The first rendering came before FCP and added nothing.
- "DevTools stripes the whole long task." No. The stripes start 50 ms in. Only the part past 50 ms is striped.
Practice exercises
Answer first, then use the hints.
One 300 ms task runs somewhere in 1,000 ms of start-up. What does the program print?
// One 300 ms task, somewhere in 1,000 ms of start-up. Taps come at random moments.
const task = 300;
const windowMs = 1000;
const overFifty = Math.max(0, task - 50) / windowMs;
console.log(`${Math.round(overFifty * 100)}% of taps wait more than 50 ms`);25% of taps wait more than 50 ms. The first 250 ms of the task are the slow moments, out of 1,000.
Three tasks, with FCP at 1,000 ms and TTI at 1,470 ms. What does the program print?
// Three tasks, with FCP at 1000 ms and TTI at 1470 ms.
function totalBlockingTime(tasks, fcp, tti) {
let total = 0;
for (const { start, duration } of tasks) {
const from = Math.max(start, fcp);
const to = Math.min(start + duration, tti);
if (to - from > 50) total += to - from - 50;
}
return total;
}
const tasks = [{ start: 900, duration: 150 }, { start: 1200, duration: 45 }, { start: 1400, duration: 70 }];
console.log(`TBT: ${totalBlockingTime(tasks, 1000, 1470)} ms`);TBT: 20 ms. The 150 ms task only has 50 ms after FCP, so it adds 0. The 45 ms task isn't long. The 70 ms task adds its last 20 ms.
In the taps playground, pick Forty tasks. How many of the 24 taps landed inside a task?
15. The other 9 came between tasks or after the work. Even the ones that landed waited 11.9 ms at most.
In the TBT playground, pick Eight tasks. What TBT did Lighthouse report, in ms?
11.6 ms. Lighthouse's load had fewer of the eight tasks over 50 ms than the course's load. Right at the line, small changes decide.
Open the course's trace of the eight-task page in Chrome's DevTools. What does DevTools call the event right under each long Task?
Timer fired. Each task was a timer from setTimeout. Under it come Function call and readReviews, the function reviews.js queued.
For Time to Interactive, how many network requests may still be in flight while Lighthouse calls the network quiet?
2: ALLOWED_CONCURRENT_REQUESTS. While no more than 2 requests are in flight, Lighthouse counts the network as quiet.
Check your understanding
For each question, ask which task a tap would land in, and how much of it would still be left.
Question 1 of 7Why does the Long Tasks API draw its line at 50 ms, when the goal is an answer in 100 ms?
Choose an answer to see the explanation.
Question 2 of 7Asha taps at a random moment while one 400 ms task runs. On average, how long does her tap wait for it?
Choose an answer to see the explanation.
Question 3 of 7What does this TBT program print?
Read the code, then predict// Two tasks, with FCP at 150 ms and TTI at 460 ms. function totalBlockingTime(tasks, fcp, tti) { let total = 0; for (const { start, duration } of tasks) { const from = Math.max(start, fcp); const to = Math.min(start + duration, tti); if (to - from > 50) total += to - from - 50; } return total; } const tasks = [{ start: 100, duration: 120 }, { start: 400, duration: 60 }]; console.log(`TBT: ${totalBlockingTime(tasks, 150, 460)} ms`);Choose an answer to see the explanation.
Question 4 of 7The same work runs as forty 10 ms tasks, each changing the page. Which is true?
Choose an answer to see the explanation.
Question 5 of 7In DevTools' Main track, what does the striped red part of a task show?
Choose an answer to see the explanation.
Question 6 of 7Where should you measure TBT?
Choose an answer to see the explanation.
Question 7 of 7On the one-task page, the first rendering ran as one long task, before the first contentful paint. Which tool reported it?
Choose an answer to see the explanation.
Key takeaways
- A long task keeps the main thread busy for 50 ms or more. A tap that lands in one waits for the rest of it.
- The line is at 50 ms so that the wait, plus the tap's own handler, fits in 100 ms.
- At a random moment, a tap waits half a task on average. Split the work, and each wait shrinks with the tasks.
- TBT is the time a tap would have waited more than 50 ms, from FCP to TTI. Lighthouse measures it with no taps at all.
- DevTools marks each task over 50 ms with a red triangle, and stripes the part past 50 ms.
- Long animation frames also catch slow screens made of short tasks, and drawing work the Long Tasks API misses.
Remember the one-liner.
A tap waits for the rest of whatever task is running, so keep every task short, and read TBT as the time a tap would wait too long.
Sources
The facts on this page come from these specs, docs and source files:
- Long Tasks API, W3C.
- Measure performance with the RAIL model, web.dev.
- Optimize long tasks, web.dev.
- main_thread_scheduler_impl.cc, Chromium, Chromium on GitHub.
- content/browser/devtools/protocol/input_handler.cc, Chromium, Chromium on GitHub.
- Total Blocking Time (TBT), web.dev.
- Total Blocking Time, Chrome for Developers.
- core/computed/metrics/total-blocking-time.js, Lighthouse 13.6.0, GoogleChrome on GitHub.
- core/computed/metrics/interactive.js, Lighthouse 13.6.0, GoogleChrome on GitHub.
- TBTUtils.ts, Chrome DevTools, ChromeDevTools on GitHub.
- Performance features reference, Chrome for Developers.
- Are long JavaScript tasks delaying your Time to Interactive?, web.dev.
- WarningsHandler.ts, Chrome DevTools, ChromeDevTools on GitHub.
- ThreadAppender.ts, Chrome DevTools, ChromeDevTools on GitHub.
- Long Animation Frames API, Chrome for Developers.
- Long Animation Frames, W3C.
- Long Animation Frames API explainer, W3C on GitHub.
Coming next: Yielding to the main thread. It shows how to give the browser a turn between pieces of work, so taps and frames get in.