Total Blocking Time (TBT) measures the total time between First Contentful Paint and Time to Interactive during which the main thread is blocked long enough to prevent input responsiveness. TBT is a lab metric that strongly correlates with the field metric INP (Interaction to Next Paint). It accounts for 30% of the Lighthouse performance score.
TBT Thresholds
| Rating | TBT Value |
|---|---|
| Good | Under 200ms |
| Needs Improvement | 200ms - 600ms |
| Poor | Over 600ms |
TBT is measured in Lighthouse and lab tools only -- there is no CrUX field equivalent. However, high TBT almost always predicts poor INP in the field.
How TBT Is Calculated
The browser's main thread processes JavaScript, layout, paint, and other tasks sequentially. Any task longer than 50ms is considered a "long task." TBT sums the blocking portion of each long task (the time beyond 50ms).
For example, three tasks of 80ms, 120ms, and 45ms produce:
- Task 1: 80ms - 50ms = 30ms blocking
- Task 2: 120ms - 50ms = 70ms blocking
- Task 3: 45ms = 0ms blocking (under 50ms threshold)
- Total TBT: 100ms
Finding Long Tasks
Chrome DevTools Performance Tab
- Record a page load in the Performance tab
- Look for tasks with red triangles in the Main thread flame chart -- these exceed 50ms
- Expand the task to see which JavaScript functions consumed the most time
- Focus on tasks during the FCP-to-TTI window, since only these count toward TBT
Lighthouse Treemap
Run Lighthouse and click "View Treemap" to see a visual breakdown of your JavaScript bundles by size and coverage. Unused code (highlighted in red) is a prime candidate for removal or lazy loading.
Reducing TBT
Code Splitting
Load only the JavaScript needed for the current page. Modern bundlers support route-based code splitting:
// React lazy loading
const Dashboard = React.lazy(() => import('./Dashboard'));
// Dynamic import for non-critical features
document.querySelector('.expand-btn').addEventListener('click', async () => {
const { initChart } = await import('./chart-module.js');
initChart();
});
Defer Third-Party Scripts
Analytics, chat widgets, and ad scripts are the biggest TBT offenders. Load them after the page becomes interactive:
<!-- Bad: blocks main thread during load -->
<script src="https://chat-widget.example.com/loader.js"></script>
<!-- Better: defer until after parsing -->
<script src="https://chat-widget.example.com/loader.js" defer></script>
For scripts that do not need DOM access during load, use requestIdleCallback to schedule them during idle periods.
Break Up Long Tasks
Use setTimeout(fn, 0) or scheduler.yield() to break monolithic functions into smaller chunks that yield back to the main thread:
async function processLargeDataset(items) {
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
if (i % 100 === 0) {
await new Promise(resolve => setTimeout(resolve, 0)); // Yield to main thread
}
}
}
Move Work Off the Main Thread
Use Web Workers for computationally heavy tasks like data parsing, image processing, or sorting large datasets. Web Workers run on a separate thread and do not block the main thread.
Relationship Between TBT and INP
TBT predicts how responsive the page will be to user interaction. A page with 800ms TBT will almost certainly fail INP because the main thread is frequently blocked. Fixing TBT issues (long tasks, heavy JavaScript) directly improves INP in the field.
Monitoring TBT
Run Lighthouse CI in your deployment pipeline with a TBT budget. A reasonable starting budget is 300ms, tightened to 200ms as you optimize. Track TBT trends across releases to catch regressions from new feature code or dependency updates.