Lighthouse audit total-blocking-time · Performance

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

View raw .md for LLMs / your notes

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 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.

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?

RangeVerdict
≤ 200 msGood
200–600 msNeeds improvement
> 600 msPoor

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:

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

2. Defer non-critical scripts

<!-- 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:

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:

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:

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


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

Audit your page now

Paste your URL, get scores plus a CLAUDE.md plan for Claude Code.

Run audit →