sfw/fix
TBT > 200ms high

Total Blocking Time (TBT) too high

Long main-thread tasks between first paint and interactivity sum past 200ms, making the page feel frozen to early clicks and taps.

What you see

Total Blocking Time — 0.6 s

Minimize main-thread work · Reduce JavaScript execution time

What’s actually happening

Lighthouse flags TBT in the red and the page feels sticky right after it appears — you click a button or menu and nothing happens for a beat. The screen has painted, but the main thread is jammed parsing and running JavaScript, so it can't respond to input. TBT carries 30% of the Lighthouse performance score and is the lab stand-in for field INP, so a bad number here usually means bad real-world responsiveness too.

Common causes

  • Large first-party JS bundles parsed and executed on load (often an un-code-split SPA)
  • Third-party scripts — tag managers, analytics, A/B tools, chat widgets, ad tags — running long tasks on the main thread
  • Hydration of a server-rendered framework (Next/Nuxt/etc.) doing a single big synchronous pass
  • Expensive work in a long task that never yields, e.g. parsing a huge JSON blob or building a big DOM in one go
  • Polyfills and duplicate dependencies shipped to modern browsers that don't need them

How to fix it

  1. Find the long tasks in a traceOpen Chrome DevTools → Performance, record a load, and look for tasks with the red corner flag (>50ms) in the Main track. The Bottom-Up and Call Tree tabs attribute the time to specific scripts. This tells you what to cut before you start cutting.
  2. Code-split and defer non-critical JSBreak large bundles with dynamic `import()` and route-based splitting so only above-the-fold code runs at load. Add `defer` (or `type=module`) to scripts, and lazy-load below-the-fold components so their JS doesn't compete during the critical window.
  3. Tame third-party scriptsLoad analytics/chat/ads after interaction or on idle (requestIdleCallback), or move them into a web worker with Partytown. Audit which tags are actually needed — a single chat widget can add 200ms+ of blocking on its own.
  4. Break up long tasks so the thread can yieldSplit monolithic work into chunks and yield with `await scheduler.yield()` (or `setTimeout`/`postTask`) so the browser can handle input between chunks. Even keeping each task under 50ms removes it from the TBT total entirely.
  5. Ship less to modern browsersDrop legacy polyfills via a modern build target, dedupe dependencies, and tree-shake. Less code parsed is the most direct lever on TBT — every KB of JS is parse + compile + execute time on the main thread.

Stop it recurring

Set a JS budget in CI (e.g. Lighthouse CI assertions on TBT and total byte weight) so regressions fail the build instead of reaching production.

Related errors