Fix Total Blocking Time (TBT): Unblock Your Main Thread
Learn what Total Blocking Time measures, why it kills interactivity, and practical strategies to reduce TBT for snappy, responsive pages.
Total Blocking Time (TBT) quantifies how unresponsive your page is during loading. Every millisecond the main thread is blocked is a millisecond your users can't interact with your page.
Why TBT Matters
- Directly affects perceived performance — users feel lag when clicking buttons or typing
- Correlates with INP — Google's Interaction to Next Paint metric
- Lighthouse weight — TBT accounts for 30% of the overall Lighthouse performance score
| TBT | Rating | User Experience |
|---|---|---|
| ≤ 200ms | Good | Page feels snappy |
| 200–600ms | Needs Improvement | Slight lag on interactions |
| > 600ms | Poor | Buttons feel broken, typing lags |
How to Diagnose TBT Issues
Chrome DevTools Performance Panel
- Open DevTools → Performance
- Click Record, then reload the page
- Look for long yellow bars (JavaScript) in the main thread
- Any task over 50ms contributes to TBT — the "blocking" portion is the time beyond 50ms
Lighthouse Treemap
Run Lighthouse and click "View Treemap" to see which scripts consume the most bytes and execution time.
How to Fix High TBT
1. Code-Split Your JavaScript
Don't ship one massive bundle. Split by route and lazy-load:
// ❌ BAD — imports everything upfront
import { HeavyChart } from "./charts";
// ✅ GOOD — loads only when needed
const HeavyChart = React.lazy(() => import("./charts"));
For Next.js:
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("./charts"), { ssr: false });
2. Defer Third-Party Scripts
Analytics, chat widgets, and ad scripts are often the biggest culprits:
<!-- ❌ BAD — blocks the main thread immediately -->
<script src="https://analytics.example.com/tracker.js"></script>
<!-- ✅ GOOD — loads after HTML parsing -->
<script src="https://analytics.example.com/tracker.js" defer></script>
Better yet, delay non-essential scripts until after user interaction:
// Load chat widget only after first user interaction
document.addEventListener("click", () => {
const script = document.createElement("script");
script.src = "https://chat-widget.example.com/widget.js";
document.body.appendChild(script);
}, { once: true });
3. Break Up Long Tasks
If you have computation that takes > 50ms, break it into chunks:
// ❌ BAD — one long 200ms task
items.forEach(item => processItem(item));
// ✅ GOOD — yield to the main thread between chunks
async function processInChunks(items, chunkSize = 50) {
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
chunk.forEach(item => processItem(item));
// Yield to let the browser handle user input
await new Promise(resolve => setTimeout(resolve, 0));
}
}
4. Use Web Workers for Heavy Computation
Offload CPU-intensive work off the main thread entirely:
// main.js
const worker = new Worker("/workers/data-processor.js");
worker.postMessage(rawData);
worker.onmessage = (e) => updateUI(e.data);
// workers/data-processor.js
self.onmessage = (e) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
5. Minimize DOM Size
Large DOM trees (over 1,500 nodes) make JavaScript operations slower. Every querySelectorAll, style recalculation, and layout pass is proportional to DOM size.
- Virtualize long lists (react-window, tanstack-virtual)
- Remove hidden elements from the DOM instead of using
display: none - Paginate content instead of rendering everything
6. Optimize CSS Selectors and Layout Thrashing
Avoid reading layout properties and then writing styles in a loop:
// ❌ BAD — forces layout recalculation every iteration
elements.forEach(el => {
const height = el.offsetHeight; // read → forces layout
el.style.width = height + "px"; // write → invalidates layout
});
// ✅ GOOD — batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight);
elements.forEach((el, i) => {
el.style.width = heights[i] + "px";
});
Quick Wins Checklist
- Run Lighthouse treemap to find your biggest scripts
- Code-split routes and lazy-load heavy components
- Add
deferto all non-critical<script>tags - Delay third-party widgets until user interaction
- Break tasks over 50ms into smaller chunks
- Keep DOM under 1,500 nodes
Ready to stop wasting ad spend?
Track your landing page performance, monitor Core Web Vitals, and calculate exactly how much slow pages cost you.
Start Free — No Credit Card