Detailed guide

Tbt Optimization Guide

View contents

Complete TBT Optimization Guide: Eliminating Main Thread Blocking for Responsive Pages

Introduction

Total Blocking Time (TBT) measures the cumulative blocking time caused by Long Tasks between First Contentful Paint (FCP) and Time to Interactive (TTI). A high TBT means your page feels unresponsive during the critical loading phase.

This guide provides advanced optimization strategies to reduce TBT, based on official Chrome Developers documentation and web.dev best practices.

New to TBT? Start with our beginner-friendly guide: 📖 Read: TBT Explained

Understanding Long Tasks in Depth

What Makes a Task “Long”?

The browser’s main thread handles everything: parsing HTML, executing JavaScript, handling user input, and painting pixels. When a single task takes longer than 50 milliseconds, it becomes a “Long Task” that blocks the main thread.

Main Thread Timeline:
┌────────────────────────────────────────────────────────────────┐
│ [Parse HTML] [Execute JS - 150ms] [Paint] [User Click Ignored] │
│              └─── Long Task ───┘          └─ Blocked input ──┘ │
└────────────────────────────────────────────────────────────────┘

TBT Calculation Deep Dive

For each Long Task, the blocking portion is the time exceeding 50ms:

// Example: Calculating TBT from task durations
const tasks = [
  { duration: 250 },  // 200ms blocking (250 - 50)
  { duration: 40 },   // 0ms blocking (< 50ms threshold)
  { duration: 90 },   // 40ms blocking (90 - 50)
  { duration: 155 },  // 105ms blocking (155 - 50)
];

const tbt = tasks.reduce((total, task) => {
  const blocking = Math.max(0, task.duration - 50);
  return total + blocking;
}, 0);

// TBT = 200 + 0 + 40 + 105 = 345ms

TBT’s Impact on Lighthouse Score

According to Chrome Developers documentation, TBT contributes 30% to your overall Lighthouse performance score:

Metric Weight Good Threshold
FCP 10% < 1.8s
Speed Index 10% < 3.4s
LCP 25% < 2.5s
TBT 30% < 200ms
CLS 25% < 0.1

A TBT improvement from 600ms to 200ms can significantly boost your overall score.

Strategy 1: Break Up Long Tasks

The most effective way to reduce TBT is to break large tasks into smaller chunks that yield to the main thread.

Using setTimeout for Yielding

// ❌ Bad: One long synchronous task
function processItems(items) {
  items.forEach(item => {
    heavyComputation(item);  // Blocks main thread
  });
}

// ✅ Good: Yielding between chunks
async function processItemsYielding(items) {
  const CHUNK_SIZE = 10;

  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    const chunk = items.slice(i, i + CHUNK_SIZE);

    chunk.forEach(item => heavyComputation(item));

    // Yield to main thread
    await new Promise(resolve => setTimeout(resolve, 0));
  }
}

Using requestIdleCallback

For non-critical work, use requestIdleCallback to run during browser idle time:

function processInIdleTime(tasks) {
  function processNext(deadline) {
    while (deadline.timeRemaining() > 0 && tasks.length > 0) {
      const task = tasks.shift();
      processTask(task);
    }

    if (tasks.length > 0) {
      requestIdleCallback(processNext);
    }
  }

  requestIdleCallback(processNext);
}

Using scheduler.yield() (Modern Browsers)

The new Scheduler API provides explicit yielding:

async function processWithScheduler(items) {
  for (const item of items) {
    heavyComputation(item);

    // Yield to allow user input processing
    if ('scheduler' in window) {
      await scheduler.yield();
    }
  }
}

Strategy 2: Optimize JavaScript Loading

Code Splitting

Load only the JavaScript needed for initial render:

// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
  },
};

Dynamic Imports

Defer non-critical features:

// ❌ Bad: Import everything upfront
import { heavyFeature } from './heavyFeature';

// ✅ Good: Dynamic import when needed
async function loadFeature() {
  const { heavyFeature } = await import('./heavyFeature');
  heavyFeature();
}

// Only load when user interacts
button.addEventListener('click', loadFeature);

Defer and Async Attributes

<!-- Critical: Load and execute in order -->
<script src="critical.js"></script>

<!-- Defer: Download parallel, execute after HTML parsing -->
<script src="app.js" defer></script>

<!-- Async: Download parallel, execute when ready -->
<script src="analytics.js" async></script>

Strategy 3: Use Web Workers for Heavy Computation

Move CPU-intensive work off the main thread entirely:

Creating a Web Worker

// worker.js
self.onmessage = function(e) {
  const result = heavyComputation(e.data);
  self.postMessage(result);
};

function heavyComputation(data) {
  // Complex calculations here
  return processedData;
}

Using the Worker

// main.js
const worker = new Worker('worker.js');

worker.onmessage = function(e) {
  displayResults(e.data);
};

// Send work to worker - doesn't block main thread
worker.postMessage(largeDataset);

When to Use Web Workers

Use Case Web Worker? Reason
Image processing ✅ Yes CPU-intensive, no DOM needed
Data parsing (JSON) ✅ Yes Can be slow for large files
Sorting large arrays ✅ Yes O(n log n) can block
DOM manipulation ❌ No Workers can’t access DOM
Small calculations ❌ No Overhead not worth it

Strategy 4: Optimize Third-Party Scripts

Third-party scripts are often the biggest contributors to TBT.

Audit Third-Party Impact

Use Chrome DevTools Performance panel:

  1. Open DevTools → Performance tab
  2. Record page load
  3. Look for Long Tasks (red corners)
  4. Identify which scripts cause them

Lazy Load Third-Party Scripts

// Load chat widget only when user scrolls near footer
const observer = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) {
    loadChatWidget();
    observer.disconnect();
  }
});

observer.observe(document.querySelector('#footer'));

function loadChatWidget() {
  const script = document.createElement('script');
  script.src = 'https://chat-widget.example.com/widget.js';
  document.body.appendChild(script);
}

Use Resource Hints

<!-- Establish early connections -->
<link rel="preconnect" href="https://analytics.example.com">
<link rel="dns-prefetch" href="https://ads.example.com">

Self-Host Critical Third-Party Resources

<!-- Instead of external CDN -->
<script src="https://cdn.example.com/library.js"></script>

<!-- Self-host for better control -->
<script src="/js/vendor/library.js"></script>

Strategy 5: Optimize React/Vue/Angular Applications

React Optimizations

// Use React.lazy for code splitting
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  );
}

// Use useDeferredValue for expensive updates
function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  return <ExpensiveList query={deferredQuery} />;
}

Vue Optimizations

// Async components
const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
);

// Defer reactive updates
import { computed, shallowRef } from 'vue';

const expensiveData = shallowRef(null);
const processedData = computed(() => {
  // Only runs when explicitly needed
  return heavyProcess(expensiveData.value);
});

Angular Optimizations

// Lazy load routes
const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module')
      .then(m => m.DashboardModule)
  }
];

// Use OnPush change detection
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedComponent {}

Measuring TBT Improvements

Chrome DevTools Performance Panel

  1. Open DevTools (F12)
  2. Go to Performance tab
  3. Click Record, then reload page
  4. Stop recording after page loads
  5. Look for:
    • Long Tasks (tasks with red corners)
    • Main thread activity (yellow = JavaScript)
    • Total blocking time in summary

Lighthouse CI

Automate TBT tracking in CI/CD:

# .github/workflows/lighthouse.yml
- name: Run Lighthouse
  uses: treosh/lighthouse-ci-action@v9
  with:
    urls: |
      https://example.com/
    budgetPath: ./budget.json
// budget.json
{
  "timings": [
    {
      "metric": "total-blocking-time",
      "budget": 200
    }
  ]
}

Real User Monitoring

Track TBT proxy (Long Tasks) in production:

// Observe Long Tasks
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) {
      console.log('Long Task:', entry.duration, 'ms');
      // Send to analytics
      analytics.track('long_task', {
        duration: entry.duration,
        startTime: entry.startTime
      });
    }
  }
});

observer.observe({ entryTypes: ['longtask'] });

Common Pitfalls and Solutions

Pitfall 1: Synchronous Data Processing

// ❌ Problem: Parsing large JSON blocks main thread
const data = JSON.parse(largeJsonString);

// ✅ Solution: Use streaming or Web Worker
const worker = new Worker('json-parser-worker.js');
worker.postMessage(largeJsonString);

Pitfall 2: Expensive React Renders

// ❌ Problem: Re-rendering expensive component on every state change
function Dashboard() {
  const [filter, setFilter] = useState('');
  return <ExpensiveChart data={allData} filter={filter} />;
}

// ✅ Solution: Memoize expensive components
const MemoizedChart = React.memo(ExpensiveChart);

function Dashboard() {
  const [filter, setFilter] = useState('');
  const filteredData = useMemo(() =>
    filterData(allData, filter), [filter]
  );
  return <MemoizedChart data={filteredData} />;
}

Pitfall 3: Unoptimized Images Triggering Layout

<!-- ❌ Problem: Large image decode blocks main thread -->
<img src="huge-image.jpg">

<!-- ✅ Solution: Use decode() and proper sizing -->
<img src="optimized.webp"
     width="800" height="600"
     decoding="async"
     loading="lazy">

TBT Optimization Checklist

Before deploying, verify:

  • [ ] No Long Tasks > 150ms during page load
  • [ ] Third-party scripts loaded asynchronously
  • [ ] JavaScript bundles under 200KB (compressed)
  • [ ] Heavy computations moved to Web Workers
  • [ ] Code splitting implemented for routes/features
  • [ ] React/Vue/Angular using lazy loading
  • [ ] TBT < 200ms in Lighthouse audit

Continue optimizing your site’s performance:

📚 Back to Performance SEO Hub - Explore all performance topics


References

  1. Chrome Developers - Total Blocking Time
  2. web.dev - Total Blocking Time (TBT)
  3. Chrome Developers - Lighthouse Performance Scoring
  4. web.dev - Optimize Long Tasks

Try It Yourself

Ready to optimize your TBT?

🔧 Download UXR SEO Analyzer (Free, 100% local analysis)


Disclaimer: The analyzers in this extension are reference guides based on official Chrome Developers and web.dev documentation. They do not represent absolute truths about how search engines evaluate your content—only search engines know their internal algorithms. Use these recommendations as a starting point to improve your site.

Last updated: December 14, 2025

Related articles

Related version

Introduction

Tbt Explained

Total Blocking Time (TBT) is a critical performance metric that measures how long your page's main thread is blocked during page load

Category hub

Hub

Performance Seo Hub

Performance is a critical ranking factor and directly impacts user experience

In the same category

Detailed guide

Inp Optimization Guide

Interaction to Next Paint (INP) measures how quickly your page responds to user interactions

Detailed guide

Lcp Optimization Guide

Largest Contentful Paint (LCP) measures when the largest content element becomes visible to users