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)
- 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
Method 1: Chrome DevTools Performance Panel (Recommended)
- Open Chrome DevTools (
F12) - Navigate to "Performance" tab
- Click record button (circle icon)
- Interact with your page (scroll, click, type)
- Stop recording after 5-10 seconds
- Look for red triangles in the timeline
- Click on long tasks (bars > 50ms)
- 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
- Open Chrome DevTools (
F12) - Navigate to "Lighthouse" tab
- Select "Performance" category
- Click "Generate report"
- 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
- Visit PageSpeed Insights
- Enter your URL
- Review diagnostics section:
- "Avoid long main-thread tasks"
- "Minimize main-thread work"
- "Reduce JavaScript execution time"
- 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
- Install Web Vitals Extension
- Navigate to your site
- Interact with the page
- Check INP score (influenced by long tasks)
- 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:
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)); } }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)); } } }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:
Use defer attribute:
<!-- Defers execution until HTML parsing complete --> <script src="non-critical.js" defer></script>Use async for independent scripts:
<!-- Loads and executes asynchronously --> <script src="analytics.js" async></script>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 });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:
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 });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); };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));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:
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> ); }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> ); }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:
Load third-party scripts asynchronously:
<!-- Google Analytics --> <script async src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>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>Audit and remove unnecessary scripts:
- Review all third-party scripts
- Remove unused analytics/tracking
- Consolidate duplicate functionality
- Consider server-side alternatives
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:
Use passive event listeners:
// Tells browser it won't call preventDefault() window.addEventListener('scroll', handleScroll, { passive: true }); window.addEventListener('touchstart', handleTouch, { passive: true });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));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:
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); }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> ); }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:
Verification
After implementing fixes:
Re-run Chrome DevTools Performance:
- Record new trace
- Verify fewer red triangles
- Check that longest task < 100ms
- Confirm smoother timeline
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
Test real interactions:
- Click buttons immediately after page load
- Scroll during page load
- Type in form fields
- Verify responsive feel
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
Use Long Tasks API:
- Monitor production for long tasks
- Set up alerts for tasks > 100ms
- Track frequency and duration
Common Mistakes
- Loading all JavaScript upfront - Defer non-critical code
- Processing large datasets synchronously - Break into chunks
- Running analytics immediately - Defer until after page load
- Not using code splitting - Ship entire app on first load
- Heavy third-party scripts - Review and optimize all third-party code
- Complex event handlers - Keep handlers lightweight
- Synchronous XHR requests - Always use async
- Large DOM manipulations - Batch updates, use fragments
- Polling instead of events - Use observers and event listeners
- Not measuring real impact - Always test on real devices