Optimizing Total Blocking Time (TBT)
Total Blocking Time is the sum of "blocked" milliseconds between FCP and TTI. Any task longer than 50 ms on main thread is a Long Task; everything above 50 ms sums to TBT. Good TBT by Lighthouse: less than 200 ms (desktop), less than 300 ms (mobile simulator with 4× CPU throttling).
Diagnosis: Finding Long Tasks
First step—find what creates Long Tasks. Chrome DevTools, Performance tab: record page load, look for red bars above main thread.
Programmatically via Long Task API:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log({
name: entry.name,
duration: entry.duration,
startTime: entry.startTime,
attribution: entry.attribution,
});
}
});
observer.observe({ type: 'longtask', buffered: true });
Reason #1: Heavy JavaScript on Startup
Most common—large JS bundle parsed and executed synchronously on load. Each megabyte JS requires ~1 second parsing on average mobile device.
Solution: code splitting + lazy loading. For React:
// Before: loads all at once
import HeavyChart from './HeavyChart';
import DataTable from './DataTable';
// After: load only when needed
const HeavyChart = lazy(() => import('./HeavyChart'));
const DataTable = lazy(() => import('./DataTable'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && (
<Suspense fallback={<Skeleton />}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
Reason #2: Third-Party Scripts
GTM, chats, ad network pixels—each third-party script can create Long Tasks.
Solutions:
<!-- Instead of sync: -->
<script src="https://widget.example.com/chat.js"></script>
<!-- Use async or defer: -->
<script src="https://widget.example.com/chat.js" async></script>
<!-- Or defer loading after interaction: -->
<script>
function loadChat() {
const s = document.createElement('script');
s.src = 'https://widget.example.com/chat.js';
document.head.appendChild(s);
}
['click', 'scroll', 'keydown'].forEach(event => {
window.addEventListener(event, loadChat, { once: true });
});
</script>
Reason #3: Heavy Computations
Use Web Workers for CPU-intensive tasks like sorting, crypto, complex filters:
// worker.js
self.onmessage = function(e) {
const { data, operation } = e.data;
let result = heavyComputation(data, operation);
self.postMessage(result);
};
// main.js
const worker = new Worker('/worker.js');
worker.postMessage({ data: largeArray, operation: 'sort' });
worker.onmessage = (e) => setTableData(e.data);
Reason #4: React 18 - startTransition
Mark non-urgent updates as transitions:
import { startTransition, useState } from 'react';
function SearchPage() {
const [inputValue, setInputValue] = useState('');
const [searchQuery, setSearchQuery] = useState('');
function handleInput(e) {
const value = e.target.value;
setInputValue(value); // Urgent: user sees typing
startTransition(() => {
setSearchQuery(value); // Non-urgent: can interrupt
});
}
return (
<>
<input value={inputValue} onChange={handleInput} />
<SearchResults query={searchQuery} />
</>
);
}
Optimization Checklist
- Split JS bundle: initial < 150 kB gzip, routes lazy
- Third-party: non-critical with defer or deferred loading
- Heavy computations in Web Worker
- Long sync loops split via
scheduler.yield()(Chrome 115+) - React:
startTransitionfor non-urgent updates - Ad networks: check for 200–500 ms TBT
Timeframe
Diagnosis + plan — 1–2 business days. Code splitting + lazy loading typical React app — 3–5 business days. Full cycle: diagnosis, optimization, production INP monitoring — 1.5–3 weeks depending on app complexity.







