Reduce Total Blocking Time (TBT) for Faster Pages | OpsBlu Docs

Reduce Total Blocking Time (TBT) for Faster Pages

Lower Total Blocking Time by optimizing JavaScript execution. Covers long task identification, code splitting, web workers, and third-party script.

Total Blocking Time (TBT) measures the total time between First Contentful Paint and Time to Interactive during which the main thread is blocked long enough to prevent input responsiveness. TBT is a lab metric that strongly correlates with the field metric INP (Interaction to Next Paint). It accounts for 30% of the Lighthouse performance score.

TBT Thresholds

Rating TBT Value
Good Under 200ms
Needs Improvement 200ms - 600ms
Poor Over 600ms

TBT is measured in Lighthouse and lab tools only -- there is no CrUX field equivalent. However, high TBT almost always predicts poor INP in the field.

How TBT Is Calculated

The browser's main thread processes JavaScript, layout, paint, and other tasks sequentially. Any task longer than 50ms is considered a "long task." TBT sums the blocking portion of each long task (the time beyond 50ms).

For example, three tasks of 80ms, 120ms, and 45ms produce:

  • Task 1: 80ms - 50ms = 30ms blocking
  • Task 2: 120ms - 50ms = 70ms blocking
  • Task 3: 45ms = 0ms blocking (under 50ms threshold)
  • Total TBT: 100ms

Finding Long Tasks

Chrome DevTools Performance Tab

  1. Record a page load in the Performance tab
  2. Look for tasks with red triangles in the Main thread flame chart -- these exceed 50ms
  3. Expand the task to see which JavaScript functions consumed the most time
  4. Focus on tasks during the FCP-to-TTI window, since only these count toward TBT

Lighthouse Treemap

Run Lighthouse and click "View Treemap" to see a visual breakdown of your JavaScript bundles by size and coverage. Unused code (highlighted in red) is a prime candidate for removal or lazy loading.

Reducing TBT

Code Splitting

Load only the JavaScript needed for the current page. Modern bundlers support route-based code splitting:

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

// Dynamic import for non-critical features
document.querySelector('.expand-btn').addEventListener('click', async () => {
  const { initChart } = await import('./chart-module.js');
  initChart();
});

Defer Third-Party Scripts

Analytics, chat widgets, and ad scripts are the biggest TBT offenders. Load them after the page becomes interactive:

<!-- Bad: blocks main thread during load -->
<script src="https://chat-widget.example.com/loader.js"></script>

<!-- Better: defer until after parsing -->
<script src="https://chat-widget.example.com/loader.js" defer></script>

For scripts that do not need DOM access during load, use requestIdleCallback to schedule them during idle periods.

Break Up Long Tasks

Use setTimeout(fn, 0) or scheduler.yield() to break monolithic functions into smaller chunks that yield back to the main thread:

async function processLargeDataset(items) {
  for (let i = 0; i < items.length; i++) {
    processItem(items[i]);
    if (i % 100 === 0) {
      await new Promise(resolve => setTimeout(resolve, 0)); // Yield to main thread
    }
  }
}

Move Work Off the Main Thread

Use Web Workers for computationally heavy tasks like data parsing, image processing, or sorting large datasets. Web Workers run on a separate thread and do not block the main thread.

Relationship Between TBT and INP

TBT predicts how responsive the page will be to user interaction. A page with 800ms TBT will almost certainly fail INP because the main thread is frequently blocked. Fixing TBT issues (long tasks, heavy JavaScript) directly improves INP in the field.

Monitoring TBT

Run Lighthouse CI in your deployment pipeline with a TBT budget. A reasonable starting budget is 300ms, tightened to 200ms as you optimize. Track TBT trends across releases to catch regressions from new feature code or dependency updates.