Fixing High Total Blocking Time (TBT)
Total Blocking Time is a lab measurement from Lighthouse. It adds up how long the browser was too busy running JavaScript to respond to a visitor while the page was loading.
It is not a Core Web Vital and never appears in field data, but it is a good early warning for poor Interaction to Next Paint, and it has a heavy influence on the Lighthouse performance score. This guide shows how TBT is worked out, how to find the specific scripts responsible, and the changes that bring it down without breaking the features your site relies on.
How Total Blocking Time is calculated
A browser does most of its work on a single main thread: parsing HTML, running JavaScript, working out styles and layout, and responding to taps and clicks. While a piece of JavaScript is running, anything else has to wait. Any continuous chunk of main-thread work that lasts longer than 50 milliseconds is called a long task.
TBT only counts the part of each long task beyond the first 50 ms, because a delay under 50 ms is too short for a person to notice. It adds those excess amounts together for the period between First Contentful Paint and the point where the main thread becomes reliably quiet.
| Long task length | Counted as blocking time |
|---|---|
| 40 ms | 0 ms (not a long task) |
| 120 ms | 70 ms |
| 300 ms | 250 ms |
| Total for these three | 320 ms |
This is why one 400 ms task is much worse than eight 50 ms tasks doing the same work: the single long task contributes 350 ms of blocking time, the eight short ones contribute nothing. Most TBT work is therefore about doing less and doing it in smaller pieces.
In the Lighthouse report for mobile, TBT of 200 ms or less is shown in green, 200 to 600 ms in amber and over 600 ms in red. Those bands are Lighthouse's own scoring, not a Google ranking threshold.
Why TBT matters when it is not a Core Web Vital
Lighthouse cannot measure Interaction to Next Paint because nobody taps or clicks during a lab test. TBT is its substitute. A page with lots of long tasks during load is likely to respond slowly when real visitors try to open the menu, tap "add to basket" or type in a search box while scripts are still running.
The link is not exact. TBT only looks at the loading period, and INP covers interactions throughout the whole visit, including slow event handlers long after load. A page can have low TBT and still fail INP because one button runs an expensive handler, and a page can have high TBT but pass INP if visitors rarely interact during the first few seconds. Use TBT to spot heavy start-up JavaScript, and use field INP data to decide whether real visitors are affected. The INP guide covers interaction handlers in depth.
TBT also carries the largest weight of any metric in the Lighthouse performance score in recent versions, so reducing it is usually the quickest way to lift that score. Remember that the score is a lab summary; the Core Web Vitals pass or fail comes from real users.
Finding the scripts behind the blocking time
Start with the lab section of the Core Web Vitals checker or a Lighthouse report. The diagnostics that list long main-thread tasks, JavaScript execution time per script and the impact of third-party code are your first map. Note the scripts that appear near the top of each list.
Recording a trace
- Open Chrome DevTools, go to the Performance panel and set CPU throttling to a 4x or 6x slowdown to imitate a mid-range phone.
- Click the reload-and-record button. When it finishes, look along the Main track for grey task bars marked with a red corner. Each one is a long task.
- Click a long task and open the Bottom-Up tab, grouped by domain or by URL. This shows which files and which functions used the time.
- Look at the Summary for each task: time split between scripting, rendering and painting. Heavy "Recalculate style" or "Layout" inside a task suggests a script that is changing the page in a loop.
Grouping by owner
Sort what you find into three groups: third-party tags (analytics, advertising, chat, reviews, heatmaps, consent tools), your own framework and application code, and platform or plugin code you did not write. Each group has a different fix, and on many small-business sites the third-party group is the largest by a wide margin.
// Paste into the console, then reload, to log long tasks as they happen
new PerformanceObserver((list) => {
for (const task of list.getEntries()) {
console.log('long task', Math.round(task.duration), 'ms at', Math.round(task.startTime));
}
}).observe({ type: 'longtask', buffered: true });
The newer Long Animation Frames API gives richer detail, including the script source for each slow
frame, and is what the web-vitals library uses for INP attribution in the field.
Third-party tags
Every tag you add runs on the same main thread as your site. Tag managers make it easy to add tags and hard to remember why each one is there.
- Audit the container. Open your tag manager and list every tag with its owner and purpose. Tags for campaigns that have ended, tools nobody logs into and duplicate analytics are common.
- Fire after consent. Under UK GDPR and PECR, non-essential cookies and similar tracking generally need consent before they are set, so advertising and analytics tags that set them should not run until the visitor agrees. This is not legal advice, but it is also a performance win, because those tags do not run for visitors who decline.
- Load on interaction. Chat widgets, video players and review carousels can be replaced by a lightweight placeholder that loads the real script when the visitor clicks, hovers or scrolls near it.
- Delay until idle. Tags that must run for everyone but are not urgent can wait until after the load event.
// Load a chat widget only when the visitor asks for it
document.querySelector('#chat-button').addEventListener('click', () => {
const s = document.createElement('script');
s.src = 'https://chat.example.com/widget.js';
s.async = true;
document.head.appendChild(s);
}, { once: true });
The third-party tags guide goes further into facades, consent-based loading and moving tags to server-side tagging.
Your own JavaScript
Ship less on first load
- Code splitting. Use dynamic
import()so code for a feature only downloads when that feature is used, and split bundles by route so the home page does not carry the checkout. - Remove unused libraries. A bundle analyser shows what is inside each file. Large date libraries, whole icon sets and utility libraries imported in full are frequent finds.
- Drop old polyfills. If your build targets browsers that are years out of date, it may be adding code modern browsers do not need.
- Reduce hydration. Frameworks that render on the server and then "hydrate" in the browser run a lot of JavaScript at start-up. Hydrate only interactive parts, use server components or islands where your framework supports them, and keep static content static.
Break up what remains
Work that genuinely has to happen during load can be split so the browser can respond between pieces. Yielding hands control back to the main thread briefly:
function yieldToMain() {
if (globalThis.scheduler?.yield) return scheduler.yield();
return new Promise((resolve) => setTimeout(resolve, 0));
}
async function initWidgets(widgets) {
for (const widget of widgets) {
widget.init();
await yieldToMain();
}
}
scheduler.yield() is not yet available in every browser, so feature-detect it and fall
back to setTimeout as above. For heavy calculations that do not touch the page, such as
parsing a large data file or filtering thousands of products, move the work into a Web Worker so it runs
off the main thread altogether.
defer or async to a script changes when it downloads
and starts, not how long it runs. It can improve First Contentful Paint, but a 500 ms task is still a
500 ms task. To reduce TBT the work itself has to shrink, move or be split.Platform notes
- WordPress. Each plugin can enqueue scripts on every page. Use a plugin that shows which scripts load where, and stop plugins loading scripts on pages that do not use them. "Delay JavaScript until interaction" features in optimisation plugins can cut lab TBT sharply, but test carefully, because delayed scripts can make the first real interaction slower. See Core Web Vitals in WordPress.
- Shopify. Installed apps inject scripts through app embeds and theme code. Disable app embeds you do not use and check for leftover code from removed apps. See Core Web Vitals in Shopify.
- React, Next.js and similar. Hydration and large client bundles are the usual cause. See Core Web Vitals in Next.js and Nuxt.
The fix, step by step
Step 1
Record the load and list the long tasks
Run Lighthouse on mobile settings three to five times and note the median TBT, then record a page load in the DevTools Performance panel with the CPU slowed by four to six times. Mark every long task on the main track between the first paint and the point where activity stops. Write down the start time and length of each so you can compare the same list after every change.
Step 2
Attribute each task to a script
Click each long task and use the Bottom-Up view, grouped by domain or URL, to see which file and function used the time. Sort the results into third-party tags, your own application or framework code, and plugin or platform code. Add the size of each file from the Network panel. The biggest few items on this list are where nearly all of the reduction will come from.
Step 3
Remove scripts nobody needs
Go through the tag manager, theme and plugin list and remove every script without a clear current owner and purpose: ended campaigns, duplicate analytics, abandoned heatmaps, old chat tools and uninstalled apps that left code behind. Deleting a script is the only change that removes its cost entirely, so do this before any optimisation, and keep a record of what you removed in case something needs restoring.
Step 4
Delay third-party tags until they are needed
Set advertising and analytics tags to fire only after consent has been given. Replace chat widgets, embedded videos and review carousels with lightweight placeholders that load the real script on click or when scrolled into view. For tags that must run for everyone, trigger them after the load event rather than immediately, and check in the Performance panel that the long tasks have moved out of the loading period.
Step 5
Ship less JavaScript on first load
Run
a bundle analyser on your own code and split large bundles by route and by feature with dynamic import().
Replace heavy libraries with smaller ones or native browser features, stop importing whole icon or utility
packages, and update your browser targets so modern browsers are not sent unnecessary polyfills. On framework
sites, reduce how much of the page hydrates and keep static sections as plain HTML.
Step 6
Break up the tasks that must stay
For
start-up work that cannot be removed or delayed, split loops and initialisation into smaller chunks and
yield to the main thread between them, using scheduler.yield() where it exists and a setTimeout
fallback elsewhere. Move pure calculation, such as parsing or sorting large data sets, into a Web Worker.
Aim for each remaining task to finish well within about 50 milliseconds.
Step 7
Retest under the same conditions
Rerun Lighthouse the same number of times, on the same settings, and compare the median TBT and your long-task list with the baseline. Check that features still work, particularly consent, checkout and forms. Because TBT is lab-only, confirm the real-world effect by watching field INP in the Core Web Vitals checker and your own monitoring over the following weeks, since that is the figure Google assesses.
Stopping blocking time creeping back
- A tag approval rule. Nothing goes into the tag manager or theme without an owner, a purpose, a loading rule (after consent, on interaction or after load) and a review date.
- A JavaScript budget. Set a limit for script size per template and have your build fail or warn when a change exceeds it.
- Lighthouse in the pipeline. Run Lighthouse against key templates on each deployment and alert when median TBT rises above an agreed limit.
- Field INP monitoring. Keep real-user monitoring running with attribution, so you see which scripts slow real interactions, not just the lab load.
For a closer look at splitting work up, see the long tasks guide.
1. What is a good Total Blocking Time?
Lighthouse shows TBT in green at 200 milliseconds or less on its mobile test, amber from 200 to 600 milliseconds and red above that. These are Lighthouse scoring bands rather than a Google ranking threshold, because TBT is a lab measurement and is not one of the Core Web Vitals. Treat 200 milliseconds on a throttled mobile test as a sensible target, and judge real-world responsiveness by field INP, which should be 200 milliseconds or less.
2. Is TBT a Core Web Vital?
No. The Core Web Vitals are Largest Contentful Paint, Interaction to Next Paint and Cumulative Layout Shift, all measured from real Chrome users. TBT is only available in lab tools such as Lighthouse and PageSpeed Insights, where it stands in for INP because no one interacts with the page during a test. It is worth fixing because high TBT often goes with poor INP and because it strongly affects the Lighthouse performance score, but it does not decide a pass or fail on its own.
3. Why is my TBT different every time I run Lighthouse?
TBT depends on how fast the test machine processes JavaScript, and that varies with whatever else the machine is doing, including other browser tabs and extensions when you test locally. Third-party scripts that load different adverts or experiments on each visit add more variation. Run the test three to five times and use the median, test in an incognito window without extensions, and compare before and after results using the same tool and settings.
4. Will adding defer to my scripts reduce TBT?
Usually not by much. The defer attribute changes when a script downloads and runs, which can speed up the first paint, but the script still does the same amount of work on the main thread once it starts. If it runs a 300 millisecond task, that task still counts. To reduce TBT you need to remove the script, delay it until after load or until the visitor needs it, make it do less, or break its work into smaller pieces.
5. Do third-party scripts count towards my TBT?
Yes. Every script on the page shares the same main thread, so time spent running analytics, advertising, chat, review or consent scripts counts exactly the same as your own code. On many small-business sites third-party tags make up the largest share of blocking time. Lighthouse has a diagnostic that estimates the main-thread time used by each third-party provider, which is a good way to see which tags cost the most before deciding what to remove or delay.
6. My TBT is high but my field INP is good. Should I still fix it?
It is lower priority. Field INP is what Google assesses and what visitors feel, so if it is comfortably under 200 milliseconds at the 75th percentile, real visitors are not suffering much. It is still worth trimming obvious waste, because high TBT often shows up as poor INP on slower phones or after you add another script. Check the mobile field data specifically, since desktop visitors on fast machines can hide a problem that phone users face.
7. Can a Web Worker fix high TBT?
For the right kind of work, yes. A Web Worker runs JavaScript on a separate thread, so heavy calculations there do not block the main thread and do not count as long tasks. It suits pure data processing, such as parsing, sorting, filtering or compressing. It cannot touch the page directly, so code that updates the DOM must stay on the main thread. Libraries exist that run some third-party tags in a worker, but test them carefully, because not every tag works that way.
The Core Web Vitals checker runs a Lighthouse lab test next to real-user field data from the Chrome UX Report, and points to the scripts and code behind heavy main-thread work and slow responsiveness.
Open Core Web Vitals Fixes โ