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
- Navigate to PageSpeed Insights
- Enter your website URL
- Click "Analyze"
- Review TBT score in metrics section
- 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
- Open your website in Chrome
- Press
F12to open DevTools - Navigate to "Performance" tab
- Enable "Enable advanced paint instrumentation" in settings
- Click record and refresh page
- Stop recording after page load
- 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
- Open Chrome DevTools (
F12) - Navigate to "Lighthouse" tab
- Select "Performance" category
- Click "Generate report"
- Review TBT score
- 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
- Open Chrome DevTools (
F12) - Press
Cmd+Shift+P(Mac) orCtrl+Shift+P(Windows) - Type "coverage" and select "Show Coverage"
- Click record and refresh page
- Review unused JavaScript percentage
- 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
- In Chrome DevTools Performance tab
- After recording, look for "Bottom-Up" or "Call Tree" tab
- Group by domain
- 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:
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(); });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>Remove unused code:
- Use tree-shaking
- Remove unused dependencies
- Audit with Coverage tool
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:
Audit third-party scripts:
- Remove unnecessary scripts
- Question if each script provides value
- Consider alternatives with less overhead
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>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>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:
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)); } } }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); } }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); };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:
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 reflowRead 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'; // WriteUse 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); } }Use CSS containment:
.widget { contain: layout style paint; }
Fix 5: Lazy Load Non-Critical Resources
Defer loading until needed:
Lazy load images:
<img src="image.jpg" loading="lazy" alt="Description">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(); });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:
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]);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> ); }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:
Profile and optimize hot paths:
// Identify slow functions with console.time console.time('operation'); slowOperation(); console.timeEnd('operation');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);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:
Test with Chrome DevTools:
- Record Performance profile
- Verify fewer/shorter long tasks (red markers)
- Check TBT improvement
Run Lighthouse:
- Generate performance report
- Verify TBT < 200ms
- Check improved JavaScript execution time
Test interactivity:
- Click buttons during page load
- Verify responsive interaction
- Test on slower devices/connections
Monitor continuously:
- Track TBT over time
- Monitor real user INP data
- Alert on regressions
Common Mistakes
- Large JavaScript bundles - Split code and lazy load
- Blocking third-party scripts - Load async or defer
- Heavy framework code - Optimize rendering, use code splitting
- Layout thrashing - Batch DOM reads and writes
- Synchronous operations - Use async patterns, Web Workers
- Not profiling - Guess at optimizations instead of measuring
- Ignoring third-party impact - Focus only on first-party code
- 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