Skip to content

SpeedPart of: Render-blocking CSS and JavaScript

Total Blocking Time: find the long tasks and cut main-thread work

Total Blocking Time adds up how long JavaScript froze the page during load. Find the long tasks behind it, then remove, defer, split and offload the work until TBT is under 200 ms.

getReport teamUpdated 27 Sept 202613 min read

Total Blocking Time (TBT) is how long the page could not respond to a tap or key press while it loaded: Lighthouse takes every main-thread task longer than 50 ms, counts the part over 50 ms, and adds them up. Under 200 ms is good on the mobile run. You reduce it by running less JavaScript during load, running it later, and cutting the rest into pieces under 50 ms. TBT is one half of the render-path problem described in render-blocking resources: the CSS, JavaScript and fonts that delay your page; that guide is about what delays the first paint, this one is about what freezes the page after it. By the end you will know which scripts cause your TBT and the order of fixes that shrinks it.

Quick answer

  • A long task is any piece of main-thread work over 50 ms. During it, the page cannot respond.
  • TBT is the sum of the time over 50 ms in every long task during load. Aim under 200 ms on the mobile run. Its field counterpart is Interaction to Next Paint (INP), also good under 200 ms.
  • The speed test's long-tasks finding names the scripts; Chrome DevTools → Performance shows them as blocks with a red corner.
  • Fix in this order: remove scripts nobody needs, defer the rest, split bundles so pages load only their code, break loops up with scheduler.yield(), move heavy computation to a web worker.
  • On WordPress, most long tasks come from sliders, builders and tag managers. A "delay JavaScript" option lowers the lab number; check the field INP after.

What Total Blocking Time measures

A browser tab has one main thread, and it does everything: runs your JavaScript, calculates styles, lays out the page, paints it, and handles every tap and scroll, one thing at a time. While a script runs for 400 ms, a tap that arrives 10 ms in waits 390 ms for an answer.

The 50 ms line comes from how people perceive delay. A response within about 100 ms feels instant. If the thread is never busy for more than 50 ms at a stretch, any input can be handled within the next 50 ms, and the 100 ms budget holds.

Total Blocking Time turns that into one number. For every long task between First Contentful Paint and the moment the page goes quiet, take the part over 50 ms and add them up. Three tasks of 80, 250 and 400 ms give 30 + 200 + 350 = 580 ms of TBT. Lighthouse counts up to 200 ms as good on mobile, and TBT carries 30% of the Lighthouse performance score, more than any other metric (Lighthouse scoring documentation, Chrome for Developers).

TBT is a lab metric: it exists because a test has no visitor to tap. The field metric Google uses as a Core Web Vital is Interaction to Next Paint, the delay between a real tap and the next frame at the 75th percentile of visits. The two usually move together, because both are made of long tasks, but INP also covers the slow "Add to cart" ten seconds after load, which TBT never sees.

What causes long tasks

Long tasks come from ordinary things:

  • A large JavaScript bundle. Before a line runs, the browser has to parse and compile all of it, and on a phone that alone can be a long task.
  • A plugin that walks the whole DOM on DOMContentLoaded, or a slider that initialises every slide at once.
  • A tag manager firing fifteen tags in one go.
  • A framework hydrating a page it already rendered on the server.
  • Style and layout work on a very large DOM, covered in DOM size: why 3,000 nodes is a problem.

Each is worse on a phone. Lighthouse's mobile run simulates a CPU about four times slower than a desktop, which is why a script that feels instant on your laptop shows up as a 250 ms task in the report.

How getReport checks it

The speed module reads three Lighthouse audits from the same throttled mobile run: Total Blocking Time is the metric that scores the result, "Avoid long main-thread tasks" lists each task over 50 ms with the script that started it, and "Minimize main-thread work" splits the thread's total time into categories such as script evaluation, style and layout, and parsing. The field INP comes from the Chrome UX Report when the page or origin has enough traffic.

The long tasks finding opened: how many tasks over 50 ms the run found and their total duration, the why-it-matters text, the fix list, and the technical line naming each script with the milliseconds of its task
The technical line is the work order: longest task first, with the script that ran it.

The technical line under the long-tasks finding is the list to work from. The main-thread breakdown tells you whether the time is mostly script (usual) or style and layout (a DOM-size problem). "Unattributable" tasks are the browser's own work, typically parsing a huge HTML document, and they shrink when the page does.

How to reduce Total Blocking Time, step by step

1. See the tasks yourself

The finding gives you names; the Performance panel gives you the shape. In Chrome DevTools → Performance, set CPU throttling to "4× slowdown", press record, reload the page, and stop after it settles. In the Main track, long tasks are grey blocks with a red triangle in the corner and red hatching on the part over 50 ms. Click one: the Summary tab says what ran, and the Bottom-Up tab, grouped by URL, lists which files used the most time.

Record a second time and click the thing visitors click most (the menu, a filter, "Add to cart"). A long task under that click is the one that sets your INP.

2. Remove what does not need to run

Before optimising anything, delete. Scripts that load on every page for a feature on one page (form validation, a product gallery's zoom, the map on the About page). Two copies of jQuery. A polyfill bundle for browsers the site no longer supports. Third-party tags with no owner: third-party scripts: tag managers, chat and ads has the procedure for auditing them. Every removed file is a task that no longer exists. The unused code checker shows how much of each script the page never ran.

3. Defer what runs too early

A script in <head> without attributes runs the moment it arrives, in the middle of parsing. defer moves its execution to after the HTML is parsed, in document order; async runs it whenever it arrives, for scripts that depend on nothing else. Async vs defer explains which to use when.

HTML
<!-- Before: blocks parsing and runs before the page exists -->
<script src="/js/app.js"></script>

<!-- After: downloads in parallel, runs after parsing, in order -->
<script defer src="/js/vendor.js"></script>
<script defer src="/js/app.js"></script>

Deferring does not shorten a task; it moves it. The gain is that the first paint no longer waits behind it. Work that can wait until the browser is idle (analytics setup, a "recently viewed" widget) can go to requestIdleCallback. Safari does not enable it by default, so keep a fallback:

JavaScript
// Run non-urgent setup when the browser has nothing better to do
const whenIdle = window.requestIdleCallback || ((cb) => setTimeout(cb, 1));
whenIdle(() => initRecentlyViewed());

4. Split the bundle so each page loads its own code

One app.js with every feature means every page parses code for features it does not have. Bundlers split on dynamic import(): the code inside becomes a separate file, fetched and compiled only when the line runs.

JavaScript
// The gallery is only needed when the visitor opens it
document.querySelector('.gallery-open')?.addEventListener('click', async () => {
  const { openGallery } = await import('./gallery.js');
  openGallery();
});

Route-based splitting is on by default in Next.js, Nuxt and SvelteKit. The trap there is importing a heavy library (a chart, a date picker, a rich-text editor) in a shared layout instead of in the page that uses it.

5. Break long work into pieces

Some work has to happen: rendering 200 search results, filtering a product list, initialising a page of widgets. A loop that does it in one go is one long task. The same loop that yields to the browser every 50 ms is many short ones, and a tap that arrives in between is handled at once:

JavaScript
// Yield to the main thread; scheduler.yield() where the browser has it, setTimeout elsewhere
function yieldToMain() {
  if (globalThis.scheduler && typeof scheduler.yield === 'function') {
    return scheduler.yield();
  }
  return new Promise((resolve) => setTimeout(resolve, 0));
}

// Initialise widgets in chunks of about 50 ms
async function initWidgets(widgets) {
  let deadline = performance.now() + 50;
  for (const widget of widgets) {
    widget.init();
    if (performance.now() > deadline) {
      await yieldToMain();
      deadline = performance.now() + 50;
    }
  }
}

scheduler.yield() puts the rest of your work at the front of the queue, so it resumes right after any pending input instead of behind everything a setTimeout competes with. It is available in Chrome and Edge 129+ and Firefox 142+, but not in Safari (MDN), which is what the fallback is for.

The same idea applies to event handlers: show the response first, then do the work. Toggle the "added" state on the button, yield, then update the mini-cart, the recommendations and the analytics call.

6. Move heavy computation off the thread

Sorting 5,000 rows, filtering a large catalogue, parsing a big JSON file: none of it touches the screen, so none of it needs the main thread. A Web Worker runs in parallel and reports back with a message:

JavaScript
// main.js: send the data, render when the worker answers
const worker = new Worker('/js/filter-worker.js');
worker.addEventListener('message', (event) => renderResults(event.data));
worker.postMessage({ products, query: 'running' });
JavaScript
// filter-worker.js: no DOM access, just the work
self.addEventListener('message', (event) => {
  const { products, query } = event.data;
  const matches = products.filter((p) => p.name.toLowerCase().includes(query));
  self.postMessage(matches);
});

The worker's task can take as long as it likes; the main thread stays free for taps and scrolls.

7. Hydrate less

Sites built with React, Vue or Svelte and rendered on the server ship HTML that looks finished, then run the framework to attach behaviour to it. On a large page this hydration is often the longest task. The frameworks' answers are islands and partial hydration (Astro, Fresh), server components that ship no JavaScript for static parts (React Server Components in Next.js), and lazy hydration for components below the fold. A content page should ship a small fraction of the JavaScript the interactive checkout needs.

Platform notes

WordPress

Two scripts core can load on every page are rarely needed: the emoji detection script and, on sites with plugins built for older jQuery, jQuery Migrate. In functions.php or a small plugin:

PHP
// Stop the emoji detection script and styles
add_action('init', function () {
    remove_action('wp_head', 'print_emoji_detection_script', 7);
    remove_action('wp_print_styles', 'print_emoji_styles');
});

// Drop jQuery Migrate on the front end; test the site afterwards, old plugins may need it
add_action('wp_default_scripts', function ($scripts) {
    if (!is_admin() && isset($scripts->registered['jquery'])) {
        $scripts->registered['jquery']->deps = array_diff($scripts->registered['jquery']->deps, ['jquery-migrate']);
    }
});

The bigger tasks come from plugins: sliders, page builders' frontend scripts, animation add-ons and a tag manager with too many tags. The WordPress plugin checker shows what each plugin loads; Perfmatters or Asset CleanUp can then switch a plugin's scripts off on pages that do not use it.

"Delay JavaScript execution" in WP Rocket, Perfmatters and LiteSpeed Cache holds listed scripts until the first interaction. TBT drops because the tasks leave the load, but they all run at the first tap, which can make that tap slow. Exclude the scripts the first interaction needs (menu, cart, search) and check the field INP a month later.

Shopify

Apps are the usual source. Each installed app can inject a script on every page; uninstall what you do not use, and check theme.liquid and Online Store → Themes → Customize → App embeds for leftovers from removed apps.

Static sites and frameworks

Use the framework's bundle analyser (next build prints per-route sizes; vite-bundle-visualizer and webpack-bundle-analyzer draw a treemap) and start with the largest chunk on the most visited page.

How to verify

  • Re-run the speed test. TBT should be under 200 ms on the mobile run, the long-tasks finding lists fewer and shorter tasks, and the ones left name your own code. Keep the previous report link for the before and after.
  • Record the Performance panel again at 4× CPU throttling: no red-flagged block over 100 ms during load, and the click from step 1 shows a short task followed by a paint.
  • After 28 days, the Core Web Vitals checker shows the field INP at the 75th percentile, the number Google uses.

Common mistakes

  • Testing on the laptop. Always measure with CPU throttling and trust the report's mobile run over a desktop test.
  • Deferring a script and calling it done. The task is as long as before, just later. If it still runs before the first tap, INP does not change. Remove, split or chunk it too.
  • Yielding on every loop iteration. Each yield costs a little time; yield every 50 ms of work, not every item.
  • Moving DOM work to a worker. Workers cannot touch the DOM. Send them data, and render the result on the main thread in a short task.
  • Delaying all JavaScript on WordPress, including the menu. The lab score jumps and the first tap freezes. Exclude what the first interaction needs.

Questions people ask

What causes a high Total Blocking Time?

Too much JavaScript running during load, in pieces longer than 50 ms. The usual sources are large bundles that must be parsed and compiled, third-party tags from a tag manager, sliders and page-builder scripts that initialise everything at once, and framework hydration. Open the long-tasks finding in the speed test: its technical line names the script behind each task, longest first, so you know where to start.

Why is Total Blocking Time high on mobile but fine on desktop?

Because the mobile test runs on a much slower processor. Lighthouse's mobile run simulates a CPU about four times slower than the desktop run, so a 40 ms task on desktop becomes roughly 160 ms on mobile and 110 ms of it counts as blocking time. Real mid-range phones behave the same way. Fix for the mobile number; desktop improves with it.

What is the difference between Total Blocking Time and INP?

Total Blocking Time is a lab measure of how long the main thread was blocked during page load, with no real visitor involved. INP is a field measure from real Chrome users: how long the page took to respond to their taps and key presses over the whole visit. Both are made of long tasks, so lowering TBT usually improves INP, but only INP is a Core Web Vital.

Does Total Blocking Time affect SEO?

Not directly. Google's ranking systems use field Core Web Vitals, and for responsiveness that is INP, not TBT. TBT matters for SEO as an early warning: it is the closest lab stand-in for INP, and it is 30% of the Lighthouse performance score. A TBT over 600 ms, which Lighthouse rates as poor, is a strong hint that phones will see a slow INP too.

Check your site before and after Check