Total Blocking Time (TBT) | Blue Frog Docs

Total Blocking Time (TBT)

Diagnose and fix main thread blocking issues to improve interactivity and user responsiveness

Total Blocking Time (TBT)

What This Means

Total Blocking Time (TBT) measures the total time the main thread is blocked for long enough to prevent input responsiveness. During blocked time, the browser cannot respond to user interactions like clicks, taps, or keyboard input.

TBT Thresholds

  • Good: < 200ms (green)
  • Needs Improvement: 200ms - 600ms (yellow)
  • Poor: > 600ms (red)

Impact on Your Business

User Experience:

  • High TBT creates "frozen" or unresponsive interface
  • Users click buttons that don't respond immediately
  • Creates frustration and perception of poor quality
  • Especially problematic on mobile devices

User Engagement:

  • Unresponsive interfaces increase bounce rates
  • Users abandon sites that feel sluggish
  • Reduces time on site and page views
  • Impacts mobile user satisfaction significantly

Relationship to INP:

  • TBT is a lab metric that predicts Interaction to Next Paint (INP)
  • INP is a Core Web Vital (replacing FID)
  • Improving TBT improves INP
  • Both measure responsiveness to user input

What TBT Measures

TBT quantifies the blocking time during page load:

  • Measures from First Contentful Paint (FCP) to Time to Interactive (TTI)
  • Sums all "long tasks" (tasks > 50ms)
  • Only counts the blocking portion (task time - 50ms)

Example:

  • Task 1: 80ms → Blocking time: 30ms (80 - 50)
  • Task 2: 120ms → Blocking time: 70ms (120 - 50)
  • Task 3: 40ms → Blocking time: 0ms (< 50ms threshold)
  • Total TBT: 100ms (30 + 70 + 0)

Common Causes

JavaScript execution:

  • Heavy JavaScript frameworks
  • Unoptimized third-party scripts
  • Synchronous operations
  • Large bundle sizes

Main thread work:

  • Complex DOM manipulation
  • Layout/reflow operations
  • Style calculations
  • Rendering operations

How to Diagnose

Method 1: PageSpeed Insights

  1. Navigate to PageSpeed Insights
  2. Enter your website URL
  3. Click "Analyze"
  4. Review TBT score in metrics section
  5. Scroll to "Diagnostics" for specific issues:
    • "Minimize main thread work"
    • "Reduce JavaScript execution time"
    • "Avoid enormous network payloads"

What to Look For:

  • TBT duration in milliseconds
  • Scripts contributing to blocking time
  • Main thread work breakdown
  • Long tasks identified

Method 2: Chrome DevTools Performance

  1. Open your website in Chrome
  2. Press F12 to open DevTools
  3. Navigate to "Performance" tab
  4. Enable "Enable advanced paint instrumentation" in settings
  5. Click record and refresh page
  6. Stop recording after page load
  7. Review the flame chart for:
    • Red triangles (long tasks > 50ms)
    • Yellow bars (scripting)
    • Purple bars (rendering/painting)

What to Look For:

  • Tasks longer than 50ms (marked with red)
  • Which scripts cause long tasks
  • Time spent in different activities
  • Call stack for expensive functions

Method 3: Lighthouse

  1. Open Chrome DevTools (F12)
  2. Navigate to "Lighthouse" tab
  3. Select "Performance" category
  4. Click "Generate report"
  5. Review TBT score
  6. Check diagnostics section:
    • JavaScript execution time
    • Main thread work breakdown
    • Third-party code blocking time

What to Look For:

  • TBT score and classification
  • Scripts with high execution time
  • Opportunities to reduce blocking
  • Specific function calls taking time

Method 4: Coverage Tool

  1. Open Chrome DevTools (F12)
  2. Press Cmd+Shift+P (Mac) or Ctrl+Shift+P (Windows)
  3. Type "coverage" and select "Show Coverage"
  4. Click record and refresh page
  5. Review unused JavaScript percentage
  6. Click files to see unused code highlighted in red

What to Look For:

  • Percentage of unused JavaScript
  • Large files with high unused code
  • Third-party scripts with waste
  • Opportunities for code splitting

Method 5: Third-Party Impact Analysis

  1. In Chrome DevTools Performance tab
  2. After recording, look for "Bottom-Up" or "Call Tree" tab
  3. Group by domain
  4. Identify third-party scripts consuming time

What to Look For:

  • Which third-party scripts block most
  • Analytics/advertising script impact
  • Social media widget overhead
  • Unnecessary third-party code

General Fixes

Fix 1: Optimize JavaScript Execution

Reduce JavaScript execution time:

  1. Code split large bundles:

    // Instead of importing everything
    import { everything } from 'large-library';
    
    // Import only what you need
    import { specificFunction } from 'large-library/specific';
    
    // Or use dynamic imports
    button.addEventListener('click', async () => {
      const module = await import('./feature.js');
      module.init();
    });
    
  2. Defer non-critical JavaScript:

    <!-- Critical scripts -->
    <script src="critical.js"></script>
    
    <!-- Defer non-critical -->
    <script src="analytics.js" defer></script>
    <script src="chat-widget.js" defer></script>
    
  3. Remove unused code:

    • Use tree-shaking
    • Remove unused dependencies
    • Audit with Coverage tool
  4. Minify and compress:

    # Use Terser for JavaScript minification
    terser input.js -o output.min.js -c -m
    

Fix 2: Optimize Third-Party Scripts

Third-party scripts often cause most blocking:

  1. Audit third-party scripts:

    • Remove unnecessary scripts
    • Question if each script provides value
    • Consider alternatives with less overhead
  2. Load third-party scripts asynchronously:

    <!-- Async loading -->
    <script src="https://example.com/widget.js" async></script>
    
    <!-- Or defer -->
    <script src="https://example.com/analytics.js" defer></script>
    
  3. Use facade patterns for heavy embeds:

    <!-- Instead of embedding YouTube directly -->
    <!-- Show placeholder image with play button -->
    <!-- Load actual iframe on click -->
    <div class="video-placeholder" data-video-id="VIDEO_ID">
      <img src="thumbnail.jpg" alt="Video thumbnail">
      <button class="play-button">Play</button>
    </div>
    
  4. Self-host critical third-party code:

    • Download and host analytics scripts
    • Update periodically
    • Reduces DNS lookup and connection time

Fix 3: Break Up Long Tasks

Split work into smaller chunks:

  1. Use async/await with yielding:

    async function processItems(items) {
      for (let i = 0; i < items.length; i++) {
        processItem(items[i]);
    
        // Yield to browser every 50 items
        if (i % 50 === 0) {
          await new Promise(resolve => setTimeout(resolve, 0));
        }
      }
    }
    
  2. Use requestIdleCallback:

    function processLowPriorityWork() {
      if ('requestIdleCallback' in window) {
        requestIdleCallback(doWork);
      } else {
        setTimeout(doWork, 1);
      }
    }
    
    function doWork(deadline) {
      while (deadline.timeRemaining() > 0 && workQueue.length) {
        const work = workQueue.shift();
        work();
      }
      if (workQueue.length) {
        requestIdleCallback(doWork);
      }
    }
    
  3. Use Web Workers for heavy computation:

    // main.js
    const worker = new Worker('worker.js');
    worker.postMessage({ data: largeDataset });
    worker.onmessage = (e) => {
      console.log('Result:', e.data);
    };
    
    // worker.js
    self.onmessage = (e) => {
      const result = heavyComputation(e.data);
      self.postMessage(result);
    };
    
  4. Debounce and throttle event handlers:

    // Throttle scroll handler
    let ticking = false;
    window.addEventListener('scroll', () => {
      if (!ticking) {
        window.requestAnimationFrame(() => {
          handleScroll();
          ticking = false;
        });
        ticking = true;
      }
    });
    
    // Debounce input handler
    let timeout;
    input.addEventListener('input', (e) => {
      clearTimeout(timeout);
      timeout = setTimeout(() => {
        handleInput(e.target.value);
      }, 300);
    });
    

Fix 4: Optimize DOM Manipulation

Reduce layout thrashing and reflows:

  1. Batch DOM updates:

    // Bad - causes multiple reflows
    element1.style.width = '100px';
    element2.style.width = '200px';
    element3.style.width = '300px';
    
    // Good - batch with DocumentFragment
    const fragment = document.createDocumentFragment();
    // ... add elements to fragment
    container.appendChild(fragment); // Single reflow
    
    // Or use CSS classes
    element.classList.add('new-layout'); // Single reflow
    
  2. Read then write (avoid interleaving):

    // Bad - alternating reads and writes
    const h1 = element1.clientHeight; // Read (forces layout)
    element1.style.height = h1 + 10 + 'px'; // Write
    const h2 = element2.clientHeight; // Read (forces layout)
    element2.style.height = h2 + 10 + 'px'; // Write
    
    // Good - batch reads, then batch writes
    const h1 = element1.clientHeight; // Read
    const h2 = element2.clientHeight; // Read
    element1.style.height = h1 + 10 + 'px'; // Write
    element2.style.height = h2 + 10 + 'px'; // Write
    
  3. Use CSS transforms instead of layout properties:

    /* Bad - triggers layout */
    .animate {
      animation: move 1s;
    }
    @keyframes move {
      from { left: 0; }
      to { left: 100px; }
    }
    
    /* Good - doesn't trigger layout */
    .animate {
      animation: move 1s;
    }
    @keyframes move {
      from { transform: translateX(0); }
      to { transform: translateX(100px); }
    }
    
  4. Use CSS containment:

    .widget {
      contain: layout style paint;
    }
    

Fix 5: Lazy Load Non-Critical Resources

Defer loading until needed:

  1. Lazy load images:

    <img src="image.jpg" loading="lazy" alt="Description">
    
  2. Lazy load scripts:

    // Load script when needed
    function loadScript(src) {
      return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = src;
        script.onload = resolve;
        script.onerror = reject;
        document.head.appendChild(script);
      });
    }
    
    // Load when user interacts
    button.addEventListener('click', async () => {
      await loadScript('feature.js');
      initFeature();
    });
    
  3. Lazy load components:

    // React lazy loading
    const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
    
    function App() {
      return (
        <Suspense fallback={<div>Loading...</div>}>
          <HeavyComponent />
        </Suspense>
      );
    }
    

Fix 6: Optimize React/Framework Rendering

Framework-specific optimizations:

  1. React optimization:

    // Use React.memo for expensive components
    const ExpensiveComponent = React.memo(({ data }) => {
      return <div>{/* render */}</div>;
    });
    
    // Use useMemo for expensive calculations
    const expensiveValue = useMemo(() => {
      return computeExpensiveValue(a, b);
    }, [a, b]);
    
    // Use useCallback for function references
    const handleClick = useCallback(() => {
      // handle click
    }, [dependencies]);
    
  2. Virtualize long lists:

    import { FixedSizeList } from 'react-window';
    
    function VirtualList({ items }) {
      return (
        <FixedSizeList
          height={600}
          itemCount={items.length}
          itemSize={35}
          width="100%"
        >
          {({ index, style }) => (
            <div style={style}>{items[index]}</div>
          )}
        </FixedSizeList>
      );
    }
    
  3. Avoid unnecessary re-renders:

    // Move static content outside component
    const STATIC_DATA = { /* ... */ };
    
    function Component() {
      // Don't create new objects in render
      const [data] = useState(STATIC_DATA);
      return <div>{data.value}</div>;
    }
    

Fix 7: Reduce JavaScript Complexity

Simplify and optimize algorithms:

  1. Profile and optimize hot paths:

    // Identify slow functions with console.time
    console.time('operation');
    slowOperation();
    console.timeEnd('operation');
    
  2. Use efficient data structures:

    // Bad - O(n) lookup
    const users = [/* array of users */];
    const user = users.find(u => u.id === targetId);
    
    // Good - O(1) lookup
    const usersMap = new Map(users.map(u => [u.id, u]));
    const user = usersMap.get(targetId);
    
  3. Memoize expensive calculations:

    const memoize = (fn) => {
      const cache = new Map();
      return (...args) => {
        const key = JSON.stringify(args);
        if (cache.has(key)) return cache.get(key);
        const result = fn(...args);
        cache.set(key, result);
        return result;
      };
    };
    
    const expensiveFunction = memoize((input) => {
      // expensive calculation
      return result;
    });
    

Platform-Specific Guides

Detailed implementation instructions for your specific platform:

Platform Troubleshooting Guide
Shopify Shopify TBT Optimization
WordPress WordPress TBT Optimization
Wix Wix TBT Optimization
Squarespace Squarespace TBT Optimization
Webflow Webflow TBT Optimization

Verification

After implementing fixes:

  1. Test with Chrome DevTools:

    • Record Performance profile
    • Verify fewer/shorter long tasks (red markers)
    • Check TBT improvement
  2. Run Lighthouse:

    • Generate performance report
    • Verify TBT < 200ms
    • Check improved JavaScript execution time
  3. Test interactivity:

    • Click buttons during page load
    • Verify responsive interaction
    • Test on slower devices/connections
  4. Monitor continuously:

    • Track TBT over time
    • Monitor real user INP data
    • Alert on regressions

Common Mistakes

  1. Large JavaScript bundles - Split code and lazy load
  2. Blocking third-party scripts - Load async or defer
  3. Heavy framework code - Optimize rendering, use code splitting
  4. Layout thrashing - Batch DOM reads and writes
  5. Synchronous operations - Use async patterns, Web Workers
  6. Not profiling - Guess at optimizations instead of measuring
  7. Ignoring third-party impact - Focus only on first-party code
  8. Loading everything upfront - Lazy load non-critical resources

TBT vs INP

TBT (Lab Metric)

  • Measured during page load
  • Synthetic testing
  • Predictive of responsiveness
  • Easier to test and debug

INP (Field Metric)

  • Measures actual user interactions
  • Real User Monitoring required
  • Core Web Vital (ranking factor)
  • Reflects real user experience

Relationship:

  • Low TBT usually correlates with good INP
  • Optimizing TBT improves INP
  • Both measure main thread blocking
  • Focus on TBT for development, monitor INP in production

Additional Resources

// SYS.FOOTER