View contents
Complete TBT Optimization Guide: Eliminating Main Thread Blocking for Responsive Pages
Introduction
Total Blocking Time (TBT) measures the cumulative blocking time caused by Long Tasks between First Contentful Paint (FCP) and Time to Interactive (TTI). A high TBT means your page feels unresponsive during the critical loading phase.
This guide provides advanced optimization strategies to reduce TBT, based on official Chrome Developers documentation and web.dev best practices.
New to TBT? Start with our beginner-friendly guide: 📖 Read: TBT Explained
Understanding Long Tasks in Depth
What Makes a Task “Long”?
The browser’s main thread handles everything: parsing HTML, executing JavaScript, handling user input, and painting pixels. When a single task takes longer than 50 milliseconds, it becomes a “Long Task” that blocks the main thread.
Main Thread Timeline:
┌────────────────────────────────────────────────────────────────┐
│ [Parse HTML] [Execute JS - 150ms] [Paint] [User Click Ignored] │
│ └─── Long Task ───┘ └─ Blocked input ──┘ │
└────────────────────────────────────────────────────────────────┘
TBT Calculation Deep Dive
For each Long Task, the blocking portion is the time exceeding 50ms:
// Example: Calculating TBT from task durations
const tasks = [
{ duration: 250 }, // 200ms blocking (250 - 50)
{ duration: 40 }, // 0ms blocking (< 50ms threshold)
{ duration: 90 }, // 40ms blocking (90 - 50)
{ duration: 155 }, // 105ms blocking (155 - 50)
];
const tbt = tasks.reduce((total, task) => {
const blocking = Math.max(0, task.duration - 50);
return total + blocking;
}, 0);
// TBT = 200 + 0 + 40 + 105 = 345ms
TBT’s Impact on Lighthouse Score
According to Chrome Developers documentation, TBT contributes 30% to your overall Lighthouse performance score:
| Metric | Weight | Good Threshold |
|---|---|---|
| FCP | 10% | < 1.8s |
| Speed Index | 10% | < 3.4s |
| LCP | 25% | < 2.5s |
| TBT | 30% | < 200ms |
| CLS | 25% | < 0.1 |
A TBT improvement from 600ms to 200ms can significantly boost your overall score.
Strategy 1: Break Up Long Tasks
The most effective way to reduce TBT is to break large tasks into smaller chunks that yield to the main thread.
Using setTimeout for Yielding
// ❌ Bad: One long synchronous task
function processItems(items) {
items.forEach(item => {
heavyComputation(item); // Blocks main thread
});
}
// ✅ Good: Yielding between chunks
async function processItemsYielding(items) {
const CHUNK_SIZE = 10;
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
chunk.forEach(item => heavyComputation(item));
// Yield to main thread
await new Promise(resolve => setTimeout(resolve, 0));
}
}
Using requestIdleCallback
For non-critical work, use requestIdleCallback to run during browser idle time:
function processInIdleTime(tasks) {
function processNext(deadline) {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
const task = tasks.shift();
processTask(task);
}
if (tasks.length > 0) {
requestIdleCallback(processNext);
}
}
requestIdleCallback(processNext);
}
Using scheduler.yield() (Modern Browsers)
The new Scheduler API provides explicit yielding:
async function processWithScheduler(items) {
for (const item of items) {
heavyComputation(item);
// Yield to allow user input processing
if ('scheduler' in window) {
await scheduler.yield();
}
}
}
Strategy 2: Optimize JavaScript Loading
Code Splitting
Load only the JavaScript needed for initial render:
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};
Dynamic Imports
Defer non-critical features:
// ❌ Bad: Import everything upfront
import { heavyFeature } from './heavyFeature';
// ✅ Good: Dynamic import when needed
async function loadFeature() {
const { heavyFeature } = await import('./heavyFeature');
heavyFeature();
}
// Only load when user interacts
button.addEventListener('click', loadFeature);
Defer and Async Attributes
<!-- Critical: Load and execute in order -->
<script src="critical.js"></script>
<!-- Defer: Download parallel, execute after HTML parsing -->
<script src="app.js" defer></script>
<!-- Async: Download parallel, execute when ready -->
<script src="analytics.js" async></script>
Strategy 3: Use Web Workers for Heavy Computation
Move CPU-intensive work off the main thread entirely:
Creating a Web Worker
// worker.js
self.onmessage = function(e) {
const result = heavyComputation(e.data);
self.postMessage(result);
};
function heavyComputation(data) {
// Complex calculations here
return processedData;
}
Using the Worker
// main.js
const worker = new Worker('worker.js');
worker.onmessage = function(e) {
displayResults(e.data);
};
// Send work to worker - doesn't block main thread
worker.postMessage(largeDataset);
When to Use Web Workers
| Use Case | Web Worker? | Reason |
|---|---|---|
| Image processing | ✅ Yes | CPU-intensive, no DOM needed |
| Data parsing (JSON) | ✅ Yes | Can be slow for large files |
| Sorting large arrays | ✅ Yes | O(n log n) can block |
| DOM manipulation | ❌ No | Workers can’t access DOM |
| Small calculations | ❌ No | Overhead not worth it |
Strategy 4: Optimize Third-Party Scripts
Third-party scripts are often the biggest contributors to TBT.
Audit Third-Party Impact
Use Chrome DevTools Performance panel:
- Open DevTools → Performance tab
- Record page load
- Look for Long Tasks (red corners)
- Identify which scripts cause them
Lazy Load Third-Party Scripts
// Load chat widget only when user scrolls near footer
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadChatWidget();
observer.disconnect();
}
});
observer.observe(document.querySelector('#footer'));
function loadChatWidget() {
const script = document.createElement('script');
script.src = 'https://chat-widget.example.com/widget.js';
document.body.appendChild(script);
}
Use Resource Hints
<!-- Establish early connections -->
<link rel="preconnect" href="https://analytics.example.com">
<link rel="dns-prefetch" href="https://ads.example.com">
Self-Host Critical Third-Party Resources
<!-- Instead of external CDN -->
<script src="https://cdn.example.com/library.js"></script>
<!-- Self-host for better control -->
<script src="/js/vendor/library.js"></script>
Strategy 5: Optimize React/Vue/Angular Applications
React Optimizations
// Use React.lazy for code splitting
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}
// Use useDeferredValue for expensive updates
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
return <ExpensiveList query={deferredQuery} />;
}
Vue Optimizations
// Async components
const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
);
// Defer reactive updates
import { computed, shallowRef } from 'vue';
const expensiveData = shallowRef(null);
const processedData = computed(() => {
// Only runs when explicitly needed
return heavyProcess(expensiveData.value);
});
Angular Optimizations
// Lazy load routes
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module')
.then(m => m.DashboardModule)
}
];
// Use OnPush change detection
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedComponent {}
Measuring TBT Improvements
Chrome DevTools Performance Panel
- Open DevTools (F12)
- Go to Performance tab
- Click Record, then reload page
- Stop recording after page loads
- Look for:
- Long Tasks (tasks with red corners)
- Main thread activity (yellow = JavaScript)
- Total blocking time in summary
Lighthouse CI
Automate TBT tracking in CI/CD:
# .github/workflows/lighthouse.yml
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v9
with:
urls: |
https://example.com/
budgetPath: ./budget.json
// budget.json
{
"timings": [
{
"metric": "total-blocking-time",
"budget": 200
}
]
}
Real User Monitoring
Track TBT proxy (Long Tasks) in production:
// Observe Long Tasks
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.log('Long Task:', entry.duration, 'ms');
// Send to analytics
analytics.track('long_task', {
duration: entry.duration,
startTime: entry.startTime
});
}
}
});
observer.observe({ entryTypes: ['longtask'] });
Common Pitfalls and Solutions
Pitfall 1: Synchronous Data Processing
// ❌ Problem: Parsing large JSON blocks main thread
const data = JSON.parse(largeJsonString);
// ✅ Solution: Use streaming or Web Worker
const worker = new Worker('json-parser-worker.js');
worker.postMessage(largeJsonString);
Pitfall 2: Expensive React Renders
// ❌ Problem: Re-rendering expensive component on every state change
function Dashboard() {
const [filter, setFilter] = useState('');
return <ExpensiveChart data={allData} filter={filter} />;
}
// ✅ Solution: Memoize expensive components
const MemoizedChart = React.memo(ExpensiveChart);
function Dashboard() {
const [filter, setFilter] = useState('');
const filteredData = useMemo(() =>
filterData(allData, filter), [filter]
);
return <MemoizedChart data={filteredData} />;
}
Pitfall 3: Unoptimized Images Triggering Layout
<!-- ❌ Problem: Large image decode blocks main thread -->
<img src="huge-image.jpg">
<!-- ✅ Solution: Use decode() and proper sizing -->
<img src="optimized.webp"
width="800" height="600"
decoding="async"
loading="lazy">
TBT Optimization Checklist
Before deploying, verify:
- [ ] No Long Tasks > 150ms during page load
- [ ] Third-party scripts loaded asynchronously
- [ ] JavaScript bundles under 200KB (compressed)
- [ ] Heavy computations moved to Web Workers
- [ ] Code splitting implemented for routes/features
- [ ] React/Vue/Angular using lazy loading
- [ ] TBT < 200ms in Lighthouse audit
Related Articles
Continue optimizing your site’s performance:
- TBT Explained - Understand the basics of Total Blocking Time
- INP Optimization Guide - Improve real-world interactivity
- LCP Optimization Guide - Optimize content loading
- Speed Index Optimization Guide - Improve visual progress
📚 Back to Performance SEO Hub - Explore all performance topics
References
- Chrome Developers - Total Blocking Time
- web.dev - Total Blocking Time (TBT)
- Chrome Developers - Lighthouse Performance Scoring
- web.dev - Optimize Long Tasks
Try It Yourself
Ready to optimize your TBT?
🔧 Download UXR SEO Analyzer (Free, 100% local analysis)
Disclaimer: The analyzers in this extension are reference guides based on official Chrome Developers and web.dev documentation. They do not represent absolute truths about how search engines evaluate your content—only search engines know their internal algorithms. Use these recommendations as a starting point to improve your site.
Last updated: December 14, 2025