# Total Blocking Time (TBT): what it is and how to fix it

**Audit ID:** `total-blocking-time` · **Category:** Performance

TBT is the lab-data counterpart to Interaction to Next Paint (INP). It measures how long the main thread is blocked by long tasks during page load, a direct signal of how unresponsive your page feels.

## TL;DR

- **What:** Sum of the blocking portion (anything over 50 ms) of every long task between FCP and TTI.
- **Target:** Under 200 ms.
- **Top three fixes:** code-split heavy bundles, defer non-critical scripts, replace synchronous third-party scripts.

## What TBT measures

Any JavaScript task on the main thread that runs longer than 50 ms is a "long task." TBT sums the part of each long task that exceeds 50 ms.

Example: three tasks of 80 ms, 120 ms, 40 ms.
- 80 ms task → 30 ms over the threshold → +30 ms TBT
- 120 ms task → 70 ms over → +70 ms TBT
- 40 ms task → under threshold → +0 ms
- **Total TBT: 100 ms**

While the main thread is blocked, user input (clicks, scrolls, typing) is queued. That's why TBT is the strongest predictor of "the page feels janky."

## What's a good TBT score?

| Range | Verdict |
|---|---|
| ≤ 200 ms | Good |
| 200–600 ms | Needs improvement |
| > 600 ms | Poor |

## Most common causes

1. **Large JavaScript bundles** parsed and executed on load
2. **Hydration** in SSR/SSG frameworks running expensive React/Vue trees synchronously
3. **Third-party scripts** (analytics, chat, A/B testing, ads) executing on the main thread
4. **Unnecessary work on page load**, initializing modules the user doesn't need yet

## Five concrete fixes

### 1. Code-split by route

In Next.js, Astro, Remix, SvelteKit etc., this is mostly automatic. In a custom app:

```js
// Lazy-load heavy components instead of importing top-level
const HeavyChart = lazy(() => import('./HeavyChart'));
```

### 2. Defer non-critical scripts

```html
<!-- WRONG: blocks the parser -->
<script src="analytics.js"></script>

<!-- RIGHT: defers until after document parse -->
<script src="analytics.js" defer></script>

<!-- Even better: load after user idle -->
<script>
  window.addEventListener('load', () => {
    requestIdleCallback(() => {
      const s = document.createElement('script');
      s.src = 'analytics.js';
      document.head.appendChild(s);
    });
  });
</script>
```

### 3. Break up long tasks

If a task must run, split it into chunks that yield to the browser:

```js
async function processLargeArray(items) {
  for (const item of items) {
    processItem(item);
    // yield to the browser every 5ms
    if (performance.now() % 5 === 0) {
      await new Promise(r => setTimeout(r, 0));
    }
  }
}
```

Or use the newer `scheduler.yield()` API:

```js
async function processLargeArray(items) {
  for (const item of items) {
    processItem(item);
    if (scheduler?.yield) await scheduler.yield();
  }
}
```

### 4. Move work to a Web Worker

CPU-heavy logic (parsing, image manipulation, encryption) belongs off the main thread:

```js
const worker = new Worker('/parser.js');
worker.postMessage(rawData);
worker.onmessage = (e) => render(e.data);
```

### 5. Audit and trim third-party scripts

Use Lighthouse's "Reduce the impact of third-party code" diagnostic. Common offenders: Google Tag Manager loading 5+ tags, full Intercom/Drift widgets on first load, A/B test SDKs blocking render. Audit each and ask: does this need to load before the user interacts?

## Verification

1. Re-run Lighthouse. Target TBT < 200 ms.
2. Check the "Avoid long main-thread tasks" audit alongside TBT.
3. Use Chrome DevTools → Performance panel → record a page load → look at the main thread row. Bars longer than 50 ms (yellow/red flags) are your targets.

## Related audits

- [Largest Contentful Paint (LCP)](/audits/largest-contentful-paint), load speed
- [Reduce unused JavaScript](/audits/reduce-unused-javascript), wasted bytes
- [Render-blocking resources](/audits/render-blocking-resources), CSS/JS in the critical path

---

Audit your URL at https://lighthouse-md.com.
