How to Reduce Total Blocking Time in Lighthouse
This guide belongs to Optimizing First Input Delay (FID) and INP within Core Web Vitals & Measurement. Total Blocking Time (TBT) is Lighthouse's lab proxy for interactivity: the sum, across every long task between First Contentful Paint and Time to Interactive, of the portion of each task beyond 50ms. A 120ms task contributes 70ms; a 40ms task contributes nothing.
TBT is not a Core Web Vital, but it carries 30% of the Lighthouse performance score and correlates strongly with INP during page load — an interaction that arrives while a long task is running waits for it to finish. Lighthouse considers TBT good under 200ms on its mobile profile. Pages built on modern frameworks routinely ship 600–1500ms of TBT on that profile because hydration, third-party tags and analytics all compete for the main thread in the same few seconds.
Rapid Diagnosis
- Read the "Diagnostics" in the Lighthouse report. "Minimize main-thread work" and "Reduce JavaScript execution time" list the scripts with the most CPU time; "Avoid long main-thread tasks" lists the longest tasks with their URLs.
- Open the trace. In DevTools' Lighthouse panel, "View Trace" (or run the Performance panel with 4x CPU throttling) shows long tasks marked with red corners. Sort the Bottom-Up tab by self time, grouped by URL or by activity.
- Separate first-party from third-party. Use the "Group by product" or third-party badges in the Performance panel to see how much TBT belongs to vendors.
- Check task start points. A task that begins at a
<script>evaluation is load-time code; one that begins at a timer or a message event is often a framework scheduler or a tag manager. - Repeat three to five times. TBT varies between runs; use the median before deciding what changed.
Root Cause Analysis
1. Hydration as one giant task. Server-rendered frameworks hydrate the whole tree synchronously by default, producing a single task proportional to component count — often 300–800ms on the Lighthouse mobile profile.
2. Large script evaluation. Every script's top-level code runs in one task when it is evaluated. A 400KB bundle with heavy module initialisation produces a long evaluation task even before any component renders.
3. Third-party tags firing at load. Tag managers fire many tags on the page-view trigger, each evaluating its own script; the container can occupy the thread for hundreds of milliseconds right after load.
4. Synchronous work in startup code. Building large in-memory indexes, parsing big JSON blobs, setting up analytics with synchronous storage access, or feature-flag evaluation over large rule sets — all done eagerly at startup.
Step-by-Step Resolution
1. Defer and split third-party tags
Move non-essential tags off the load path. For tag managers, change page-view triggers for analytics-adjacent tags to fire on "Window Loaded" plus an idle delay, and load chat, survey and heatmap tools on interaction.
// Load the tag manager after the page is interactive and the browser is idle.
function loadGTM(id) {
const s = document.createElement('script');
s.src = `https://www.googletagmanager.com/gtm.js?id=${id}`;
s.async = true;
document.head.appendChild(s);
}
addEventListener('load', () => requestIdleCallback(() => loadGTM('GTM-XXXX'), { timeout: 3000 }));
// trade-off: delaying the container delays every tag, including conversion
// pixels. Keep conversion-critical tags in a separate, early container or send
// them server-side; defer only the rest.
Expected outcome: TBT falls by most of the third-party share; on tag-heavy sites this alone is often 200–400ms.
2. Make hydration incremental
Hydrate above-the-fold interactive components first and defer the rest until visible or idle. React 18's streaming and Suspense boundaries let React yield between boundaries during hydration; islands frameworks avoid hydrating static content entirely.
// Wrap below-the-fold, independently hydratable sections in Suspense boundaries.
<Suspense fallback={<ReviewsPlaceholder />}>
<Reviews productId={id} />
</Suspense>
// trade-off: each boundary adds a fallback state and potential layout shift if
// the fallback is not sized like the content. Size fallbacks precisely.
Expected outcome: the single hydration task becomes several shorter tasks; TBT from hydration typically halves. See selective hydration with React Suspense.
3. Split long startup work with yields
For your own startup code, process work in chunks and yield between them so no task exceeds 50ms.
async function buildIndex(items) {
const index = new Map();
let deadline = performance.now() + 40;
for (const item of items) {
index.set(item.id, normalize(item));
if (performance.now() > deadline) {
await (globalThis.scheduler?.yield?.() ?? new Promise((r) => setTimeout(r)));
deadline = performance.now() + 40;
}
}
return index;
}
// trade-off: chunking spreads the same CPU cost over more wall-clock time. If
// the result is needed before the first interaction, it may now arrive later;
// consider computing it on the server or in a worker instead.
Expected outcome: long tasks from startup code disappear from the "Avoid long main-thread tasks" list.
4. Ship less JavaScript on the critical path
Code-split by route and by interaction, remove unused polyfills, and replace heavy dependencies. Every kilobyte not evaluated is TBT not incurred.
Expected outcome: script evaluation tasks shrink in proportion to the code removed; dynamic imports and route-based splitting is the starting point.
Verification
Run Lighthouse five times before and after, and compare median TBT. Then assert a budget in Lighthouse CI so the gain survives:
// lighthouserc.js
module.exports = { ci: {
collect: { numberOfRuns: 5 },
assert: { assertions: {
'total-blocking-time': ['error', { maxNumericValue: 300, aggregationMethod: 'median' }],
'mainthread-work-breakdown': ['warn', { maxNumericValue: 2500 }],
} },
} };
// trade-off: CI machines vary; a budget of exactly 200ms will flake. Set the
// CI gate somewhat above the target and track the trend, tightening over time.
In the field, TBT has no direct equivalent, but INP for interactions in the first few seconds after load, and the blockingDuration sum from Long Animation Frames, should both improve.
TBT, INP and the Lighthouse Score: Keeping Them in Perspective
TBT is a load-time lab metric; INP is a whole-visit field metric. They overlap only for interactions during load. A page can have excellent TBT and poor INP if its slow interactions happen later — a filter applied after a minute of browsing — and a page with mediocre TBT can have fine INP if users rarely interact during load. Use TBT to keep startup lean and to catch regressions in CI, where it is far more stable than any interaction-based lab measurement. Use field INP, broken down by interaction and phase, to decide what to fix for users. Avoid optimising TBT for its own sake: deferring work past Lighthouse's observation window lowers TBT without helping anyone if that work then blocks the user's first click.
FAQ
Why does my TBT differ so much between runs?
Long tasks are sensitive to timing: whether a third-party script arrives before or after hydration changes how tasks line up, and CPU throttling on shared machines adds noise. Use the median of at least three runs, keep the test machine quiet, and prefer devtools throttling settings that match your target device when comparing changes.
Is TBT the same as First Input Delay?
No. FID measured the delay of the first real interaction in the field and was retired in favour of INP in 2024. TBT is a lab metric that sums blocking time over the load period, regardless of whether anyone interacts. It predicts input delay risk during load rather than measuring it.
Does yielding inside tasks always reduce TBT?
Yes, if each resulting chunk is under 50ms: TBT only counts time beyond 50ms per task, so splitting a 300ms task into six 50ms tasks reduces its contribution from 250ms to zero. The total CPU time is unchanged, which is why yielding improves responsiveness without making the work itself finish sooner.
Related
- Fixing slow first interactions during hydration — the INP side of the same startup work.
- Measuring third-party cost with the Coverage panel — sizing what each vendor costs.
- LoAF vs the Long Tasks API — the field-side view of blocking time.