← Articles
8 min read

Total Blocking Time (TBT): What It Is and How to Improve It

TBT measures how long the main thread is blocked after First Contentful Paint, preventing the page from responding to user input. Here's how it's calculated and how to bring it down.


What is TBT?

Total Blocking Time measures the total amount of time between First Contentful Paint (FCP) and Time to Interactive (TTI) where the main thread was blocked long enough to prevent input responsiveness.

The main thread is considered “blocked” any time there is a Long Task — a task that runs for more than 50 milliseconds. When a long task is running, the browser cannot interrupt it. If a user clicks, taps, or types in the middle of a long task, the browser has to wait for the task to finish before it can respond. That delay is what the user experiences as a sluggish or broken page.

TBT is a lab metric, meaning it is measured in a controlled test environment rather than from real user data. It correlates closely with Interaction to Next Paint (INP), the Core Web Vital that measures actual responsiveness in the field. A low TBT is a strong predictor of a good INP score.

How TBT is Calculated

Not every task contributes to TBT — only long tasks do. The blocking time of a long task is its duration in excess of 50 milliseconds. TBT is the sum of the blocking time for every long task that occurs in the measured window.

For example, a task that runs for 100 ms has a blocking time of 50 ms (100 − 50). A task that runs for 35 ms has a blocking time of 0 ms — it is not a long task at all.

Here is a timeline with five tasks on the main thread. Three of them are long tasks:

A timeline of five tasks on the main thread during page load
Five tasks on the main thread. Three exceed the 50 ms threshold and qualify as long tasks.

The shaded portions below show only the blocking time for each long task — the slice above 50 ms:

The same timeline with blocking time highlighted for each long task
The blocking portion of each long task (duration minus 50 ms). The sum of these is the TBT.

The full breakdown for this example:

TaskDuration (ms)Blocking time (ms)
Task one250200
Task two9040
Task three350
Task four300
Task five155105
Total Blocking Time345

Total time running tasks: 560 ms. Total blocking time: 345 ms — because only the excess above 50 ms counts.

What is a Good TBT Score?

A TBT of under 200 milliseconds on average mobile hardware is considered good. Between 200 ms and 600 ms needs improvement. Anything above 600 ms is poor.

The threshold is set for mobile because mobile CPUs are significantly slower than desktop CPUs. A site that scores 50 ms on a MacBook can easily score 500 ms on a mid-range Android device.

How to Measure TBT

TBT is a lab metric and should be measured in a controlled test environment. Real-user measurement is possible but not recommended — user interaction during a test can affect long-task timing and introduce noise.

Lab tools:

  • Lighthouse (built into Chrome DevTools and PageSpeed Insights)
  • WebPageTest

For field data, the Long Animation Frames API is a better fit than TBT. For measuring actual responsiveness in production, use INP.

How to Improve TBT

Improving TBT means reducing the amount of work the main thread does during page load, and making that work less continuous. There are four areas to focus on.

Optimize Your JavaScript

JavaScript is the most common source of long tasks. The more script the browser has to parse, compile, and execute on load, the more likely it is to produce blocking tasks.

Code splitting and lazy loading

Instead of sending the entire application bundle on every page load, split code into smaller chunks and load only what the current page needs. Bundlers like Webpack, Rollup, esbuild, and Parcel all support this. Frameworks like React, Next.js, Angular, and Vue provide lazy-loading utilities:

const Dashboard = React.lazy(() => import('./Dashboard'));

Route-based lazy loading — where each page only loads its own scripts — is the most impactful form. The initial bundle shrinks, and subsequent chunks load in parallel.

Reduce the total amount of JavaScript

Less JavaScript means fewer tasks, simpler execution, and lower parse and compile time. Keep the initial bundle under 300 KB compressed (roughly 900 KB–1.3 MB once uncompressed). Where possible, split into chunks of 50 KB or less so browsers can download them in parallel over HTTP/2.

Minify your code

Always ship minified code in production. Variable name shortening and whitespace removal cut transfer size and parse time. Every major bundler handles this automatically in production mode.

Deliver an ES6 build to modern browsers

Browsers that support ES modules do not need Babel polyfills or transpiled ES5 code. Serving a lean ES2015+ build to modern browsers — and only falling back to the ES5 build for legacy ones — cuts both bundle size and execution cost:

<!-- ES5 fallback for older browsers -->
<script nomodule src="legacy-bundle.js"></script>

<!-- ES2015+ for modern browsers -->
<script type="module" src="bundle.js"></script>

Use HTTP/2 or HTTP/3

Both protocols support multiplexed requests — the browser can download many small chunks in parallel over a single connection, rather than queuing them. This makes code splitting far more effective in practice.

Reduce Bundle Size

The size of JavaScript on the page directly affects how long tasks run. Downloading a large bundle takes longer, and the browser has to parse and compile the entire payload before any of it can run.

Audit your dependencies

Dependencies are often the biggest part of a bundle and the easiest to shrink. Tools like bundlephobia.com let you check the minified + gzipped cost of any npm package and compare alternatives:

  • moment (72.1 kB gzipped) → date-fns (tree-shakeable, pay only for what you import)
  • lodashlodash-es (tree-shakeable) or individual function imports

Prefer tree-shakeable libraries

When a library uses named ES module exports, bundlers can eliminate the functions you do not import. A library that exports everything through a single default object cannot be tree-shaken — the entire thing ends up in your bundle regardless of what you use.

Block problem packages with ESLint

If your team has settled on a preferred library, enforce it with no-restricted-imports:

{
  "rules": {
    "no-restricted-imports": [
      "error",
      {
        "paths": [
          {
            "name": "moment",
            "message": "Use date-fns instead. See https://bundlephobia.com/package/moment"
          }
        ]
      }
    ]
  }
}

This turns an accidental import into a build error, preventing bundle size regressions before they reach production.

Audit Third-Party Scripts

Analytics, chat widgets, A/B testing tools, and ad scripts all run on the main thread. They are a common source of long tasks because you have little control over what they do or when they run.

Use facades

A facade is a lightweight placeholder that looks like the real widget but loads no third-party code until the user interacts with it. When the user clicks the chat button, the real widget loads. Until then, the main thread is free.

Load third parties into a web worker with Partytown

Partytown moves third-party scripts out of the main thread entirely by running them inside a web worker. Scripts that would otherwise block your page — analytics tags, tracking pixels — execute off-thread and can no longer contribute to TBT.

Delay non-critical scripts

Scripts that are not needed for the initial render should not run during it. Use defer or async, or load them after the load event:

<!-- Does not block HTML parsing, runs after DOM is ready -->
<script defer src="analytics.js"></script>

Use smaller third-party libraries

Evaluate whether a third-party library is genuinely needed, and whether a lighter alternative exists. A script that imports a full SDK to send a single event is almost always replaceable with a direct API call.

Reduce Main Thread Work

Even after trimming JavaScript, the main thread can still be overloaded by layout calculations, style recalculations, and rendering work.

Use web workers for heavy computation

Web workers run on a separate thread. Move expensive non-UI operations — data parsing, encryption, image processing — off the main thread:

const worker = new Worker('./heavy-task.js');
worker.postMessage({ data: largeDataset });
worker.onmessage = (event) => {
  updateUI(event.data.result);
};

Avoid layout thrashing

Layout thrashing happens when JavaScript alternates between reading and writing to the DOM in a loop, forcing the browser to recalculate layout repeatedly within a single task. Batch all your DOM reads first, then apply all writes:

// Bad: read, write, read, write — forces multiple layouts
const height = element.offsetHeight;
element.style.height = height + 10 + 'px';
const newHeight = element.offsetHeight;
element.style.height = newHeight + 10 + 'px';

// Good: read everything, then write everything
const height = element.offsetHeight;
const newHeight = height + 10;
element.style.height = newHeight + 'px';

Use CSS for animations and effects

CSS animations and transitions run on the compositor thread — separate from the main thread — when they animate only transform and opacity. JavaScript-driven animations that touch layout properties like top, left, width, or height run on the main thread and contribute to TBT.

References

  1. web.dev: Total Blocking Time (TBT)
  2. Calibre: Small Bundles, Fast Pages — What To Do With Too Much JavaScript
  3. Bundlephobia: Cost of JavaScript packages

performance · core-web-vitals · javascript