---
title: "Fix Total Blocking Time for Better INP"
description: "Reduce TBT to improve your Lighthouse performance score. TBT measures main thread blocking - the lab proxy for real-world INP."
canonical_url: "https://unlighthouse.dev/learn-lighthouse/inp/total-blocking-time"
last_updated: "2025-01-18"
---

TBT accounts for 30% of your Lighthouse performance score. That's the largest weight of any single metric. A site with great LCP, CLS, and FCP can still score 70 if TBT is in the red.

Good TBT is 200ms or less on mobile. Poor is above 600ms. Desktop thresholds are tighter: 150ms good, 350ms poor. Most sites fall somewhere in the "needs improvement" zone.

<audit-impact metric="tbt">



</audit-impact>

## What's the problem?

TBT measures the invisible: time when the page looks ready but can't respond to user input.

**The calculation is specific.** Lighthouse identifies every task on the main thread between First Contentful Paint and Time to Interactive. For each task exceeding 50ms, it counts only the time *above* 50ms as blocking time. A 70ms task contributes 20ms to TBT. A 200ms task contributes 150ms.

```text
Task duration: 70ms  → TBT contribution: 20ms  (70 - 50)
Task duration: 200ms → TBT contribution: 150ms (200 - 50)
Task duration: 45ms  → TBT contribution: 0ms   (under threshold)
```

**Why this matters for real users:** TBT is a lab metric. You won't see it in field data. But it strongly correlates with INP (Interaction to Next Paint), the Core Web Vital that measures real interaction responsiveness. Fix TBT in the lab, and INP improves in the field.

**The 50ms threshold exists because** human perception research shows responses under 100ms feel instant. At 50ms task length, the browser has 50ms remaining in that 100ms window to process user input and update the screen.

## How TBT relates to INP

INP measures actual user interactions in the field. TBT measures potential for blocking during controlled lab conditions.

They target the same basic issue: main thread availability. But they measure differently:

<table>
<thead>
  <tr>
    <th>
      Aspect
    </th>
    
    <th>
      TBT (Lab)
    </th>
    
    <th>
      INP (Field)
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      When measured
    </td>
    
    <td>
      Page load to TTI
    </td>
    
    <td>
      Entire session
    </td>
  </tr>
  
  <tr>
    <td>
      Data source
    </td>
    
    <td>
      Lighthouse simulation
    </td>
    
    <td>
      Real user monitoring
    </td>
  </tr>
  
  <tr>
    <td>
      Interactions
    </td>
    
    <td>
      None (task timing)
    </td>
    
    <td>
      Clicks, taps, keypresses
    </td>
  </tr>
  
  <tr>
    <td>
      Threshold (good)
    </td>
    
    <td>
      ≤200ms
    </td>
    
    <td>
      ≤200ms
    </td>
  </tr>
</tbody>
</table>

**A site can have good TBT and bad INP** if the blocking happens after initial load (lazy-loaded features, SPA navigation, dynamic content).

**A site can have bad TBT and good INP** if users don't interact during the blocking period, or if the blocking tasks are CPU-throttled in Lighthouse more than real devices experience.

### The "TBT trap": lab vs field

Optimizing TBT to 0 in the lab is a great goal, but a green Lighthouse score alone won't guarantee green Field data. This is the **"TBT Trap"**.

- **The Phase Mismatch:** TBT measures the *loading* phase (until Time to Interactive). INP measures the *entire lifespan*. If your heavy JavaScript runs when a user opens a modal 2 minutes later, TBT won't see it, but INP will fail.
- **The "Budget Stacking" Effect:** A 50ms task seems fine for a 200ms INP target. But if you have *four* 50ms tasks back-to-back, and a user clicks during the first one, the browser might queue the input until the fourth one finishes. Suddenly, your "safe" 50ms tasks stack up to a 200ms+ delay.

Fix both. TBT catches load-time issues. INP catches runtime issues.

## How to identify

### Lighthouse report

TBT appears in the Metrics section with a score from 0-100. Expand the "Diagnostics" section to find:

- **Avoid long main-thread tasks** - Lists the longest tasks by duration
- **Reduce JavaScript execution time** - Shows script-by-script execution time
- **Minimize main-thread work** - Breaks down time by category (script, style, layout)

### Chrome DevTools performance panel

1. Open DevTools → Performance tab
2. Enable CPU throttling (4x slowdown) to match Lighthouse conditions
3. Record a page load
4. Look at the "Total Blocking Time" in the summary
5. Examine the Main thread for tasks with red corners (>50ms)

### Webpagetest

Run a test with "Capture Blocking Time" enabled. The waterfall view highlights long tasks, and the filmstrip shows exactly when the page was unresponsive.

## The fix

### 1. break up long tasks

The most direct fix: no task should exceed 50ms.

```js
// Before: 300ms blocking task
function processAllData(items) {
  return items.map(transform).filter(validate).sort(compare)
}

// After: yields every 50ms of work
async function processAllDataModern(items) {
  const results = []
  let startTime = performance.now()

  for (const item of items) {
    results.push(transform(item))

    // Yield if we've been working for 40ms
    if (performance.now() - startTime > 40) {
      await scheduler.yield()
      startTime = performance.now()
    }
  }

  return results.filter(validate).sort(compare)
}
```

For simpler cases, chunk your work:

```js
async function processInChunks(items, chunkSize = 100) {
  for (let i = 0; i < items.length; i += chunkSize) {
    const chunk = items.slice(i, i + chunkSize)
    processChunk(chunk)
    await new Promise(resolve => setTimeout(resolve, 0))
  }
}
```

### 2. defer non-critical JavaScript

Scripts that don't affect initial render should load after the page is interactive.

```html
<!-- Before: blocks parsing -->
<script src="analytics.js"></script>
<script src="chat-widget.js"></script>

<!-- After: loads after HTML parsing -->
<script src="analytics.js" defer></script>
<script src="chat-widget.js" defer></script>
```

For third-party scripts you don't control:

```js
// Load after page is idle
function loadWhenIdle(src) {
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => {
      const script = document.createElement('script')
      script.src = src
      document.body.appendChild(script)
    })
  }
  else {
    setTimeout(() => {
      const script = document.createElement('script')
      script.src = src
      document.body.appendChild(script)
    }, 2000)
  }
}

loadWhenIdle('https://cdn.chat-widget.com/widget.js')
```

### 3. remove unused code

Every kilobyte of JavaScript adds parse and compile time, even if the code never runs.

**Find unused code with Coverage tool:**

1. DevTools → More tools → Coverage
2. Click record, load page, interact
3. Red bars indicate unused bytes

**Tree-shake aggressively:**

```js
// Before: imports entire library
import _ from 'lodash'

// After: imports single function
import debounce from 'lodash/debounce'

const result = _.debounce(fn, 300)
const debouncedResult = debounce(fn, 300)
```

**Audit your dependencies:**

```bash
npx depcheck # Find unused dependencies
npx bundlephobia <package-name> # Check bundle size impact
```

Replace heavy libraries with lighter alternatives:

- moment.js (300KB) → date-fns (tree-shakeable) or dayjs (2KB)
- lodash (70KB) → native methods or individual imports
- axios (13KB) → fetch API

### 4. optimize third-party scripts

Third-party scripts often contribute 50%+ of TBT. You don't control their code, but you control when and how they load.

**Audit third-party impact:**

```js
// Log third-party task durations
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) {
      console.log(`Long task: ${entry.duration}ms`, entry.attribution)
    }
  }
})
observer.observe({ entryTypes: ['longtask'] })
```

**Strategies:**

- Load below the fold scripts on scroll, not on page load
- Use `loading="lazy"` for iframe embeds
- Self-host critical third-party scripts to control delivery
- Use resource hints (`preconnect`) for scripts you can't delay

```html
<!-- Preconnect to third-party origins -->
<link rel="preconnect" href="https://www.googletagmanager.com">
<link rel="preconnect" href="https://www.google-analytics.com">
```

### 5. web workers for heavy computation

Move CPU-intensive work off the main thread entirely.

```js
// main.js
const worker = new Worker('data-processor.js')

function analyzeData(data) {
  return new Promise((resolve) => {
    worker.postMessage(data)
    worker.onmessage = e => resolve(e.data)
  })
}

// data-processor.js (runs on separate thread)
globalThis.onmessage = (e) => {
  const result = heavyComputation(e.data)
  globalThis.postMessage(result)
}
```

Candidates for Web Workers:

- JSON parsing of large payloads
- Image/video processing
- Data sorting and filtering
- Cryptographic operations
- Search indexing

## Framework-specific solutions

<callout icon="i-logos-nextjs-icon">

**Next.js**

Use React Server Components (App Router) to eliminate client-side JavaScript:

```jsx
// app/products/page.js - Server Component by default
// No JavaScript sent to client for this component
export default async function ProductsPage() {
  const products = await fetchProducts()
  return <ProductList products={products} />
}
```

For client components, use `Suspense` boundaries:

```jsx
import { Suspense } from 'react'

export default function Page() {
  return (
    <>
      <Header />
      <Suspense fallback={<ProductsSkeleton />}>
        <Products />
      </Suspense>
    </>
  )
}
```

</callout>

<callout icon="i-logos-nuxt-icon">

**Nuxt**

Use `<ClientOnly>` to defer non-critical hydration:

```html
<template>
  <main>
    <CriticalContent />
    <ClientOnly>
      <NonCriticalWidget />
      <template #fallback>
        <WidgetPlaceholder />
      </template>
    </ClientOnly>
  </main>
</template>
```

Lazy-load components with the `Lazy` prefix:

```html
<template>
  <!-- Only loads when component enters viewport -->
  <LazyHeavyChart v-if="chartVisible" :data="chartData" />
</template>
```

</callout>

<callout icon="i-logos-react">

**React**

Use `React.lazy()` for route-level code splitting:

```jsx
import { lazy, Suspense } from 'react'

const AdminDashboard = lazy(() => import('./AdminDashboard'))

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/admin" element={<AdminDashboard />} />
      </Routes>
    </Suspense>
  )
}
```

</callout>

## Verify the fix

1. **Run Lighthouse multiple times.** TBT varies between runs due to CPU timing. Take the median of 3-5 runs.
2. **Test with CPU throttling.** DevTools → Performance → CPU: 4x slowdown matches Lighthouse mobile simulation.
3. **Check the task breakdown.** After optimization, no single task should exceed 50ms in the Performance panel.

**Expected improvement:** Reducing a single 600ms task to six 100ms tasks can drop TBT from 550ms to 300ms (each 100ms task contributes 50ms instead of one task contributing 550ms).

## Common mistakes

**Focusing only on your code.** Third-party scripts often dominate TBT. Profile before optimizing.

**Over-deferring critical scripts.** Deferring everything delays interactivity. Interactive elements need their JavaScript loaded and executed.

**Testing only on fast devices.** TBT measures blocking on throttled CPU. A task that runs in 40ms on your M1 Mac runs in 160ms on a throttled mobile simulation.

**Ignoring hydration.** SSR frameworks still need to hydrate. That hydration JavaScript contributes to TBT like any other script.

## Related issues

Often appears alongside:

- [Long-Running JavaScript](/learn-lighthouse/inp/long-running-javascript) - Individual tasks causing blocking
- [Third-Party Scripts](/learn-lighthouse/inp/third-party-scripts) - External scripts adding to TBT
- [Hydration Issues](/learn-lighthouse/inp/hydration-issues) - Framework initialization blocking

## Test your entire site

TBT varies across pages. A fast homepage with a slow checkout flow still frustrates users. [Unlighthouse](/) audits your entire site and ranks pages by TBT impact.
