Main Thread Blocking & Long Tasks | Blue Frog Docs

Main Thread Blocking & Long Tasks

Diagnose and fix main thread blocking issues and long tasks that freeze the browser and degrade user experience

Main Thread Blocking & Long Tasks

What This Means

Main thread blocking occurs when JavaScript or other tasks prevent the browser's main thread from responding to user input. Long tasks are any task that takes more than 50ms to execute, during which the browser cannot process user interactions like clicks, scrolls, or keyboard input.

Long Task Thresholds

  • Good: No tasks > 50ms
  • Acceptable: Occasional tasks 50-100ms
  • Poor: Frequent tasks > 100ms or any task > 300ms

Impact on Your Business

User Experience:

  • Page feels "frozen" or unresponsive
  • Delayed response to clicks and taps
  • Janky scrolling and animations
  • Users perceive site as broken or slow
  • Direct impact on Interaction to Next Paint (INP)

Core Web Vitals:

  • Long tasks directly degrade INP scores
  • Poor INP is a confirmed ranking factor
  • Can prevent achieving "Good" Core Web Vitals
  • Affects mobile experience more severely

Conversion Impact:

  • Users abandon interactions that feel unresponsive
  • Lost sales during checkout if buttons don't respond
  • Reduced engagement with interactive features
  • Higher bounce rates on content-heavy pages

Technical Consequences:

  • Browser may show "Page Unresponsive" warning
  • Lighthouse performance score penalties
  • Negative impact on Total Blocking Time (TBT)
  • Poor real user experience metrics

How to Diagnose

  1. Open Chrome DevTools (F12)
  2. Navigate to "Performance" tab
  3. Click record button (circle icon)
  4. Interact with your page (scroll, click, type)
  5. Stop recording after 5-10 seconds
  6. Look for red triangles in the timeline
  7. Click on long tasks (bars > 50ms)
  8. Review the "Bottom-Up" tab to see what functions consumed time

What to Look For:

  • Red triangles indicating long tasks
  • Bars colored yellow/red (JavaScript execution)
  • Tasks taking > 50ms (especially > 100ms)
  • Call stacks showing which code is responsible
  • Time spent in specific functions

Method 2: Lighthouse Performance Audit

  1. Open Chrome DevTools (F12)
  2. Navigate to "Lighthouse" tab
  3. Select "Performance" category
  4. Click "Generate report"
  5. Look for:
    • "Avoid long main-thread tasks" audit
    • "Minimize main-thread work" audit
    • Total Blocking Time (TBT) metric

What to Look For:

  • Number of long tasks detected
  • Duration of longest task
  • Which scripts caused long tasks
  • Total main thread work duration
  • Specific files and line numbers

Method 3: PageSpeed Insights

  1. Visit PageSpeed Insights
  2. Enter your URL
  3. Review diagnostics section:
    • "Avoid long main-thread tasks"
    • "Minimize main-thread work"
    • "Reduce JavaScript execution time"
  4. Check field data for real user INP scores

What to Look For:

  • Long task warnings in diagnostics
  • Main thread work breakdown
  • Scripts with long execution times
  • Real user INP data (field data)

Method 4: Web Vitals Extension

  1. Install Web Vitals Extension
  2. Navigate to your site
  3. Interact with the page
  4. Check INP score (influenced by long tasks)
  5. Click for detailed breakdown

What to Look For:

  • INP values > 200ms (poor)
  • Interaction delays
  • Which interactions were slow
  • Event handler durations

Method 5: Long Tasks API (JavaScript)

Add monitoring code to detect long tasks:

// Detect long tasks in production
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.warn('Long task detected:', {
      duration: entry.duration,
      startTime: entry.startTime,
      name: entry.name
    });
    // Send to analytics
  }
});

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

General Fixes

Fix 1: Break Up Long Tasks

Split large tasks into smaller chunks:

  1. Use setTimeout to yield to main thread:

    // Bad - blocks main thread
    function processItems(items) {
      items.forEach(item => {
        // Heavy processing
        processItem(item);
      });
    }
    
    // Good - yields to main thread
    async function processItems(items) {
      for (const item of items) {
        processItem(item);
    
        // Yield to main thread every iteration
        await new Promise(resolve => setTimeout(resolve, 0));
      }
    }
    
  2. Use scheduler.yield() (modern approach):

    async function processItems(items) {
      for (const item of items) {
        processItem(item);
    
        // Better than setTimeout
        if ('scheduler' in window && 'yield' in scheduler) {
          await scheduler.yield();
        } else {
          await new Promise(resolve => setTimeout(resolve, 0));
        }
      }
    }
    
  3. Process in batches:

    async function processBatch(items, batchSize = 10) {
      for (let i = 0; i < items.length; i += batchSize) {
        const batch = items.slice(i, i + batchSize);
        batch.forEach(processItem);
    
        // Yield after each batch
        await scheduler.yield();
      }
    }
    

Fix 2: Defer Non-Critical JavaScript

Load scripts that aren't needed immediately:

  1. Use defer attribute:

    <!-- Defers execution until HTML parsing complete -->
    <script src="non-critical.js" defer></script>
    
  2. Use async for independent scripts:

    <!-- Loads and executes asynchronously -->
    <script src="analytics.js" async></script>
    
  3. Dynamically load on interaction:

    // Load heavy library only when needed
    button.addEventListener('click', async () => {
      const module = await import('./heavy-library.js');
      module.doSomething();
    }, { once: true });
    
  4. Use requestIdleCallback for non-urgent work:

    function doNonUrgentWork() {
      if ('requestIdleCallback' in window) {
        requestIdleCallback(() => {
          // Do work during idle time
          initializeAnalytics();
        });
      } else {
        setTimeout(() => {
          initializeAnalytics();
        }, 1000);
      }
    }
    

Fix 3: Optimize JavaScript Execution

Make your code more efficient:

  1. Avoid forced synchronous layouts:

    // Bad - causes layout thrashing
    elements.forEach(el => {
      el.style.width = el.offsetWidth + 10 + 'px'; // Read then write
    });
    
    // Good - batch reads and writes
    const widths = elements.map(el => el.offsetWidth); // All reads
    elements.forEach((el, i) => {
      el.style.width = widths[i] + 10 + 'px'; // All writes
    });
    
  2. Use Web Workers for heavy computation:

    // worker.js
    self.addEventListener('message', (e) => {
      const result = heavyComputation(e.data);
      self.postMessage(result);
    });
    
    // main.js
    const worker = new Worker('worker.js');
    worker.postMessage(data);
    worker.onmessage = (e) => {
      updateUI(e.data);
    };
    
  3. Debounce expensive operations:

    function debounce(func, wait) {
      let timeout;
      return function executedFunction(...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => func(...args), wait);
      };
    }
    
    // Use for scroll, resize, input events
    window.addEventListener('scroll', debounce(() => {
      // Expensive scroll handler
    }, 100));
    
  4. Optimize loops and iterations:

    // Cache length, use const, avoid unnecessary work
    const items = document.querySelectorAll('.item');
    const length = items.length;
    
    for (let i = 0; i < length; i++) {
      const item = items[i];
      // Process item
    }
    

Fix 4: Code Split and Lazy Load

Reduce initial JavaScript bundle size:

  1. Route-based code splitting:

    // React example
    const HomePage = lazy(() => import('./pages/Home'));
    const AboutPage = lazy(() => import('./pages/About'));
    
    function App() {
      return (
        <Suspense fallback={<Loading />}>
          <Routes>
            <Route path="/" element={<HomePage />} />
            <Route path="/about" element={<AboutPage />} />
          </Routes>
        </Suspense>
      );
    }
    
  2. Component-based code splitting:

    // Load heavy components only when visible
    const HeavyChart = lazy(() => import('./HeavyChart'));
    
    function Dashboard() {
      const [showChart, setShowChart] = useState(false);
    
      return (
        <div>
          <button onClick={() => setShowChart(true)}>
            Show Chart
          </button>
    
          {showChart && (
            <Suspense fallback={<Spinner />}>
              <HeavyChart />
            </Suspense>
          )}
        </div>
      );
    }
    
  3. Analyze and split bundles:

    # Webpack Bundle Analyzer
    npm install --save-dev webpack-bundle-analyzer
    
    # Add to webpack config
    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
    
    plugins: [
      new BundleAnalyzerPlugin()
    ]
    

Fix 5: Optimize Third-Party Scripts

Third-party scripts are common culprits:

  1. Load third-party scripts asynchronously:

    <!-- Google Analytics -->
    <script async src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>
    
  2. Use facades for heavy embeds:

    <!-- Instead of embedding YouTube directly -->
    <div class="youtube-facade" data-id="VIDEO_ID">
      <img src="thumbnail.jpg" alt="Video">
      <button>Play Video</button>
    </div>
    
    <script>
    // Load real iframe only on click
    document.querySelectorAll('.youtube-facade').forEach(facade => {
      facade.addEventListener('click', () => {
        const iframe = document.createElement('iframe');
        iframe.src = `https://www.youtube.com/embed/${facade.dataset.id}?autoplay=1`;
        facade.replaceWith(iframe);
      });
    });
    </script>
    
  3. Audit and remove unnecessary scripts:

    • Review all third-party scripts
    • Remove unused analytics/tracking
    • Consolidate duplicate functionality
    • Consider server-side alternatives
  4. Self-host critical third-party resources:

    <!-- Instead of CDN that might be slow -->
    <script src="/js/vendor/library.js" defer></script>
    

Fix 6: Optimize Event Handlers

Event handlers can block the main thread:

  1. Use passive event listeners:

    // Tells browser it won't call preventDefault()
    window.addEventListener('scroll', handleScroll, { passive: true });
    window.addEventListener('touchstart', handleTouch, { passive: true });
    
  2. Throttle rapid events:

    function throttle(func, limit) {
      let inThrottle;
      return function(...args) {
        if (!inThrottle) {
          func.apply(this, args);
          inThrottle = true;
          setTimeout(() => inThrottle = false, limit);
        }
      };
    }
    
    window.addEventListener('scroll', throttle(() => {
      // Handle scroll
    }, 100));
    
  3. Remove event listeners when not needed:

    function setupListener() {
      const handler = () => { /* ... */ };
      element.addEventListener('click', handler);
    
      // Cleanup
      return () => element.removeEventListener('click', handler);
    }
    
    const cleanup = setupListener();
    // Later: cleanup();
    

Fix 7: Optimize Rendering

Reduce rendering work:

  1. Use CSS transforms instead of layout properties:

    /* Bad - triggers layout */
    .element {
      transition: width 0.3s, height 0.3s;
    }
    
    /* Good - only triggers compositing */
    .element {
      transition: transform 0.3s;
      transform: scale(1.1);
    }
    
  2. Virtualize long lists:

    // Use libraries like react-window or react-virtualized
    import { FixedSizeList } from 'react-window';
    
    function VirtualList({ items }) {
      return (
        <FixedSizeList
          height={600}
          itemCount={items.length}
          itemSize={50}
        >
          {({ index, style }) => (
            <div style={style}>{items[index]}</div>
          )}
        </FixedSizeList>
      );
    }
    
  3. Use DocumentFragment for batch DOM updates:

    const fragment = document.createDocumentFragment();
    
    items.forEach(item => {
      const div = document.createElement('div');
      div.textContent = item;
      fragment.appendChild(div);
    });
    
    // Single DOM update instead of multiple
    container.appendChild(fragment);
    

Platform-Specific Guides

Detailed implementation instructions for your specific platform:

Platform Troubleshooting Guide
Shopify Shopify Main Thread Optimization
WordPress WordPress Main Thread Optimization
Wix Wix Main Thread Optimization
Squarespace Squarespace Main Thread Optimization
Webflow Webflow Main Thread Optimization

Verification

After implementing fixes:

  1. Re-run Chrome DevTools Performance:

    • Record new trace
    • Verify fewer red triangles
    • Check that longest task < 100ms
    • Confirm smoother timeline
  2. Check Lighthouse scores:

    • Total Blocking Time (TBT) improved
    • "Minimize main-thread work" shows less time
    • "Avoid long main-thread tasks" passes
    • Overall performance score increased
  3. Test real interactions:

    • Click buttons immediately after page load
    • Scroll during page load
    • Type in form fields
    • Verify responsive feel
  4. Monitor field data:

    • Check INP in Chrome User Experience Report
    • Review Google Search Console Core Web Vitals
    • Monitor INP with Web Vitals extension
    • Track improvements over 28-day period
  5. Use Long Tasks API:

    • Monitor production for long tasks
    • Set up alerts for tasks > 100ms
    • Track frequency and duration

Common Mistakes

  1. Loading all JavaScript upfront - Defer non-critical code
  2. Processing large datasets synchronously - Break into chunks
  3. Running analytics immediately - Defer until after page load
  4. Not using code splitting - Ship entire app on first load
  5. Heavy third-party scripts - Review and optimize all third-party code
  6. Complex event handlers - Keep handlers lightweight
  7. Synchronous XHR requests - Always use async
  8. Large DOM manipulations - Batch updates, use fragments
  9. Polling instead of events - Use observers and event listeners
  10. Not measuring real impact - Always test on real devices

Additional Resources

// SYS.FOOTER