惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Project Zero
Project Zero
Security Archives - TechRepublic
Security Archives - TechRepublic
C
Cyber Attacks, Cyber Crime and Cyber Security
Security Latest
Security Latest
Scott Helme
Scott Helme
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V
Vulnerabilities – Threatpost
C
CERT Recently Published Vulnerability Notes
S
Schneier on Security
G
GRAHAM CLULEY
L
Lohrmann on Cybersecurity
D
Darknet – Hacking Tools, Hacker News & Cyber Security
I
Intezer
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
F
Full Disclosure
T
The Exploit Database - CXSecurity.com
P
Proofpoint News Feed
WordPress大学
WordPress大学
Microsoft Azure Blog
Microsoft Azure Blog
H
Help Net Security
大猫的无限游戏
大猫的无限游戏
MyScale Blog
MyScale Blog
Hacker News: Ask HN
Hacker News: Ask HN
G
Google Developers Blog
H
Heimdal Security Blog
O
OpenAI News
Hugging Face - Blog
Hugging Face - Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
L
LangChain Blog
C
Cisco Blogs
云风的 BLOG
云风的 BLOG
IT之家
IT之家
Cyberwarzone
Cyberwarzone
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Know Your Adversary
Know Your Adversary
博客园 - 聂微东
The Cloudflare Blog
C
Check Point Blog
K
Kaspersky official blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
月光博客
月光博客
T
Tor Project blog
T
Threat Research - Cisco Blogs
T
Tailwind CSS Blog
P
Proofpoint News Feed
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
A
About on SuperTechFans
小众软件
小众软件
Cloudbric
Cloudbric
A
Arctic Wolf

Lobsters

CIFSwitch: a non-universal Linux local root vulnerability RIPE NCC session fixation: poaching logins with an Atlas probe GNOME 2.20 but its Web Components Agentic Search for Context Engineering – Leonie Monigatti Garnix is shutting down [not OC] akashina.tngl.sh/jjc Concerning Emacs (and Jazz) Nitpicking the shell history scene in ‘Tron: Legacy’ What's cooking on SourceHut? Q2 2026 The tenth OpenPGP email summit Package managers that package package managers Clojure on Fennel part three: parsing WordPress at 23 Finding Miscompiles for Fun, Not Profit GitHub - creusot-rs/creusot: Creusot helps you prove your Rust code is correct. Announcing Rust 1.96.0 | Rust Blog A Love Letter to Neovim sqlite AGENTS.md Am I a Bad Friend? CSS vs. JavaScript • Josh W. Comeau Erlang Ecosystem Foundation - Supporting the BEAM community A brief note about slot access cost in Common Lisp Keyboard latency probe Rethinking the GNOME clipboard issues Back to the Building Blocks’ Building Blocks Tech Notes: Theseus: translating win32 to wasm Fast is better than slow Content-addressed Rust builds (or, what kache actually caches) Intent to Prototype: Embedding API Canada’s Bill C-22 and the security cost of collecting more data 5 PostgreSQL locking behaviors that trip people up okmij.org Stop advertising in your commits! | AksDev GitHub - mplsllc/macsurf: A modern web browser for Classic Mac OS 9 PowerPC. Real CSS3, ES5 JavaScript, native HTTPS — built with CodeWarrior on the Carbon API. Introducing DoomBench - Can Your Data Stack Run DOOM? What are some of your favourite developer tools? Building a Scalable Ingestion Pipeline with Temporal (Part 1) Converting shallow Git bundles into normal repositories Are you a member of any professional associations? What is a harmonic? An interactive comic about additive synthesis How Virtual Tables Work in the Itanium C++ ABI Using SwiftUI to Build a Mac-assed App in 2026 Rust (and Slint) on a jailbroken Kindle. ~jack/lambda-on-lambda - Serverless Haskell on AWS - sourcehut git Human proof for FOSS contributions Extremely simple internet radio controlled via IRC Announcing BABLR Splitting Konsole views from Helix to run tools | AksDev GitHub - yugr/rust-slides Serving files over HTTP three ways: synchronous, epoll, and io_uring update docs with information about building with build.py (#979) · astral-sh/python-build-standalone@c9c40c5 A Simple Makefile Tutorial On C extensions, portability, and alternative compilers Switching to Colemak | Pedro Alves Just How Bad Was The Intel IAPX432? Nix's Substituter List Is Not a Routing Table Accelerating copy_if using SIMD Lambda on Lambda: Serverless Haskell on AWS | Blog Announcing feed-repeat v1.0 Scaling Akvorado BMP RIB with sharding EYG news: A host of CLI improvements, new guides and new effects The social contract of writing JS Crossword C array types are weird; and related topics Flatpak will depend on systemd – OSnews Migrating from Go to Rust | corrode Rust Consulting A portentous reunion Vivado Licensing Options How my minimal, memory-safe Go rsync steers clear of vulnerabilities the entropy layer of a wavelet codec, on its own GitHub - nferhat/fht-compositor: A dynamic tiling Wayland compositor. Debian SE Linux and PinTheft Does bulk memmove speed up std::remove_if? (No.) 声明式部分更新 | Blog | Chrome for Developers Fully in-browser container builds Dianne Skoll's Web Site - Remind The Architecture of Open Source Applications (Volume 1)Berkeley DB Pardon MIE? - ironPeak Blog “Long-Term Support” doesn’t mean what you think Jira IS Turing-Complete May I recommend thinking of Emacs as your Fortress of Solitude hershey Floodgap Gopher-HTTP gateway gopher://thelambdalab.xyz/1cuneiforth/ HP QuickWeb, Singular And Pointless That one time I used Go panics for flow control A new suite of modern tools coming for editing and publishing RFCs From the Tabletop… The Digital Antiquarian Building a Host-Tuned GCC to Make GCC Compile Faster Are we self-sovereign PKI yet? Claw Patrol: an open-source security firewall for agents | Deno Revised^7 Report on Scheme, Large: Procedural Fascicle Draft is now public A Network Allow-List Won't Stop Exfiltration — André Graf From AFSK to Goertzel – µArt.cz Software For My New Home Server Introducing Neptune: Direct3D virtualization for QEMU AI Agent Bankrupted Their Operator While Trying to Scan DN42 - Lan Tian @ Blog mimalloc: A new, high-performance, scalable memory allocator for the modern era Making wl_shm fast The Soul of Maintaining a New Machine - Third Draft | Books in Progress What is Git made of?
Front-End’s Missing Metric: The TBT Window
Harry Roberts · 2026-06-01 · via Lobsters

Written by on CSS Wizardry.

Table of Contents

Independent writing is brought to you via my wonderful Supporters.

  1. A Quick Refresher
  2. The TBT Window
  3. The LCP Improvement That Tanked TBT
  4. Why TTI Can Move
  5. Causality Can Run Both Ways
  6. How We Fixed It
  7. Make the Window Visible
  8. Closing Thoughts
    1. Citation

An incident (and it was an incident!) on a client project got me thinking about the metrics we use and the importance of fully understanding their exact definitions. Bizarrely-defined and oft-overlooked specs sent panic around a team, but ultimately shed light on a tangible gap in our front-end tooling.

For my client, from one SpeedCurve synthetic test to the next, their Total Blocking Time (TBT) skyrocketed from 495 ms to 5,789 ms. That’s more than a tenfold increase! Naturally, it was all hands on deck to pin down the cause, and I was called in to help out.

What I found was both enlightening and calming, but it did shine a light on a gap in the way we tend to talk about modern web performance metrics.

This is the story behind a new web performance metric I like to call the TBT Window.

Need Some Help?

I help companies find and fix site-speed issues. Performance audits, training, consultancy, and more.

A Quick Refresher

Total Blocking Time is a lab metric intended to quantify how unavailable the main thread is during page load. It looks at long tasks—work that occupies the UI thread for more than 50 ms—and counts only the blocking portion of each one. A 70 ms task contributes 20 ms to TBT; a 250 ms task contributes 200 ms. The first 50 ms of any task is effectively tolerated, and anything beyond that is considered blocking time.

It is a lab-based proxy for the more real user-centric Interaction to Next Paint field metric.

This much is probably familiar, but the part that is often overlooked is that TBT is not an unbounded sum of all blocking work in the trace: it is a sum of all the blocking time between First Contentful Paint and Time to Interactive. This is key to understanding the context of the TBT metric itself.

First Contentful Paint (FCP) opens the window and is the first point in the page-load timeline at which the user can see something contentful on screen—the page is responding. Time to Interactive (TTI) closes the window and is the point at which the page is considered reliably interactive.

TBT is the blocking work that happens between those two moments.

This is slightly clumsy or even unexpected because TTI itself has largely disappeared from our day-to-day vocabulary. Lighthouse 10 removed TTI from the performance score after a deprecation process that began in Lighthouse 8, and for perfectly sensible reasons: TTI was highly variable and overly sensitive to outlier network requests and long tasks. It is also seemingly arbitrarily defined and hard to reason about:

  1. Start at First Contentful Paint (FCP).
  2. Search forward in time for a quiet window of at least five seconds, where quiet window is defined as: no long tasks and no more than two in-flight network GET requests.
  3. Search backwards for the last long task before the quiet window, stopping at FCP if no long tasks are found.
  4. TTI is the end time of the last long task before the quiet window (or the same value as FCP if no long tasks are found).

Wow.

Diagram showing the Time to Interactive calculation: from Navigation Start and FCP, the browser searches forward to a five-second quiet window, then works back to the last long task to set TTI.
TTI is found by identifying the quiet window first, then walking back to the last long task before it.

The fact that TTI has been retired from the report, however, does not mean it has stopped influencing the numbers we still care about. If you are monitoring TBT, TTI still defines the far edge of the measurement window and changes in TTI scores can impact your TBT scores even in cases you might not expect.

Hopefully now you can see the problem. Per Google’s own documentation, TTI proved overly sensitive to outlier network requests and long tasks, yet if TTI also defines the upper bound for TBT, those same outliers can increase (or decrease) the surface area you’re being tested against.

A degraded TTI score can degrade your TBT score. You’re effectively still being measured against it. This is exactly what happened to my client.

The TBT Window

Once you think of TBT as a windowed metric, a few otherwise surprising behaviours become much easier to explain. The TBT Window is the interval between FCP and TTI during which TBT is allowed to count blocking work. If FCP moves earlier, the TBT Window opens earlier, so long tasks that used to sit before FCP can suddenly fall inside the window. If FCP moves later, the opposite can happen: long tasks can fall out of scope without having been removed from the page.

The same is true at the other end. If TTI moves earlier, the window closes sooner and late long tasks may stop contributing to TBT. If TTI moves later, the window stays open for longer and more of the tail of the page load can suddenly become countable.

This is the important bit: in all of these cases, the long tasks themselves may not have changed in any meaningful way. What changed was the amount of timeline that TBT was allowed to inspect. That moving interval is what I mean by the TBT Window.

SpeedCurve dashboard for Pitchup showing JS Total Blocking Time rising from 495 ms on 25 July to 5,789 ms on 26 July, alongside Time to Interactive rising to 19.98 seconds.
The clue is that both lines move together: TBT rose as the end of its measurement window moved. View full size/quality (416KB)

In my client’s case, their initial instinct was entirely understandable: a TBT regression looks like a JavaScript regression, and the SpeedCurve chart even had the word JS in it, so the first place the team looked was their bundles. They checked for new packages, third-party changes, tag manager updates, and any new hydration or framework cost that might have slipped into the release.

It was a sensible line of investigation, but it turned nothing up and didn’t fit the facts. There had been no meaningful JavaScript-facing change in the timeframe they were looking at, and the waterfalls told a much more interesting story. There were plenty of late long tasks, but they had not materially changed between the before and after traces. What had changed was TTI.

In the waterfalls below, I have annotated the five-second quiet window in yellow and worked back to TTI which is annotated as a vertical pink line. Note that, for the most part, both waterfalls share identical hallmarks.

Side-by-side WebPageTest waterfalls comparing before and after runs, with yellow quiet windows and pink TTI markers showing the after run’s TTI much later than before.
The yellow quiet window is much later after the change, pushing TTI across almost the full waterfall.

Next, let’s look specifically at long and blocking tasks. Again, the hallmarks of main thread blocking activity remain mostly unchanged, but the sheer surface area being measured is substantially larger:

Zoomed WebPageTest main thread comparison showing broadly similar blocking activity in both traces, but a much longer measured TBT Window after the regression.
The work did not suddenly appear; the metric simply had much more of the timeline available to count.

Before the regression, the page was considered interactive before a lot of that late activity mattered. After the regression, TTI had moved out toward 20 seconds, which meant the TBT Window stretched much further across the waterfall and pulled previously out-of-scope work into the metric.

The team did have a TBT regression in terms of pure numbers, but they did not have the JavaScript regression they thought they had. This is a very important distinction because those two interpretations lead to entirely different investigations. One sends you into bundles, dependencies, hydration, and third parties; the other sends you into the definition of the metric and the conditions that moved its boundaries.

Without the TBT Window in mind, you can end up trying to optimise code that did not cause the change. The practical lesson I took from this was simple: if TBT skyrockets for no obvious JavaScript reason, check the network.

The LCP Improvement That Tanked TBT

The interesting part of this story is what moved TTI in the first place. We had been working on LCP, and had added a preload for the LCP image on the previous day. This is exactly the kind of thing I like to see: identify the important image, get it discovered earlier, help the browser fetch it sooner, improve the user-visible experience. And it worked: the LCP optimisation was doing what it was supposed to do.

But, unfortunately, it also changed the shape of the network. TTI’s calculation does not only care about long tasks; it also looks for a quiet window of at least five seconds, where quiet means no long tasks and no more than two in-flight network GET requests. By introducing just one additional request via preload, the page suddenly fell foul of the network quiet-window condition. TTI moved later, the TBT Window became much larger, and suddenly long tasks that had previously sat outside the measurement window were being included.

This is the sort of thing that makes performance work so interesting and, if we are honest, sometimes quite annoying. A perfectly sensible optimisation for one metric made a completely different metric look catastrophically worse. More specifically, an optimisation intended to improve LCP caused TTI to move later, which in turn caused TBT to include more of the existing long-task tail.

Two metrics that are, from a user-experience point of view, concerned with different things became highly dependent on each other through the small print of their specs.

This is not necessarily a criticism of Lighthouse, SpeedCurve, WebPageTest, or the metrics themselves (though I do wish TTI would be fully sunset, even as a windowing metric). It is a reminder that our tooling is doing exactly what it says on the tin, but we need to read the tin.

Why TTI Can Move

The weird thing about TTI is that it sounds more intuitive than it actually is. The name makes it feel like a direct measure of when the page becomes usable, but the calculation is far more fiddly and esoteric than that. As above, TTI is jointly dependent on long tasks and network activity.

The latter is the part of TTI that I have never liked. The intent is probably sensible: a page that is still fetching resources may not be as settled as one that has gone quiet, but the threshold itself—no more than two in-flight GET requests for five seconds—is inevitably arbitrary, and allows network behaviour to influence a metric that many people read as interactivity. Frustratingly, in a world of tag managers, tracking, ads, lazy loading, and more, forensic control over network activity is seldom within developers’ reach.

That means TTI can move because of long tasks, but it can also move because the page fails to reach that required network quietness. This is precisely what happened here—a network optimisation changed the network profile, TTI moved, and the TBT Window grew.

Consider also the case of a render-blocking <script>. Any long tasks contained within it are, by definition, exempt from inclusion in TBT as they fall before FCP. The sensible and worthwhile optimisation to make the script asynchronous will tangibly improve milestone timings, but now makes any of those long tasks candidates for inclusion in TBT. In other words, speeding up paint timings may negatively impact interactivity numbers despite no material change in the amount of main thread activity.

The long tasks did not get worse, they just became visible to the metric.

Causality Can Run Both Ways

One thing worth being clear about: an expanded TBT Window does not automatically mean the TBT regression is a false alarm. Because TTI is calculated from both network quietness and long-task quietness, a genuine increase in blocking work can itself push TTI later. Add a large task at 12 seconds, or move a cluster of tasks further down the trace, and the browser may no longer find its quiet window where it used to. TTI moves out, the TBT Window grows, and TBT rises for two reasons at once: there is more blocking work and the metric now has more timeline in which to count it.

This is a significant distinction. The TBT Window is not a way to explain away TBT, but a way to avoid guessing. If the window grows because FCP or network quietness changed, you may be looking at a windowing artefact; if the window grows because true long-task activity got worse, you have a real TBT regression with a larger surface area. Either way, plotting the window gives you the context needed to tell those scenarios apart.

How We Fixed It

We didn’t. It wasn’t a regression in any real, user-facing sense and we also had next to no control over the root cause. Reverting the preload change would have improved the numbers, but it would have materially regressed the user experience.

Instead, I advised that the client put a secondary lever in place and only treat TBT as a regression if it moves independently (or at a faster rate) than TTI. All TBT charts now contain a matching TTI score that needs to diverge beyond a certain threshold before we act. If you’re monitoring TBT, I’d suggest you do the same.

Need Some Help?

I help companies find and fix site-speed issues. Performance audits, training, consultancy, and more.

Make the Window Visible

This is where I think synthetic tooling could help developers enormously. Anywhere that plots TBT should also plot the TBT Window alongside it. Not as a new score per se, but as a sanity check or control.

The data is already there. If the tool knows FCP and TTI, and it has used them to calculate TBT, it can also show the size of the window between them. That single extra line on a chart would make a lot of investigations much easier to reason about. Tools could even correlate window growth with TBT growth and alert teams to potential false flags and red herrings:

N.B. TBT Window increased by 37% between these two tests—TBT scores may be misleading.

In the interim, if you’re already rolling your own dashboards like you can in SpeedCurve, you just need to plot each of First Contentful Paint, Total Blocking Time, and Time to Interactive on the same time series:

SpeedCurve chart titled TBT & TBT Window showing JS Total Blocking Time at 82 ms, Time to Interactive at 2.34 seconds, and First Contentful Paint at 0.78 seconds, with the FCP and TTI lines remaining largely stable from 18 to 31 May.
The FCP and TTI lines define your TBT Window; any drift between these two may explain changes in TBT, so check there first! Note how stable the TBT Window remains in this screenshot.

If TBT goes up and the TBT Window stays flat, you are probably looking at a genuine increase in blocking work; if TBT goes up and the TBT Window expands at the same time, you know to ask if or why FCP or TTI moved before assuming blocking time actually got worse.

Likewise, if TBT improves while the window shrinks, you should be careful about celebrating too quickly: you may have removed blocking work, or you may simply have stopped counting some of it.

In the screenshot above, the TBT spike around 19–20 May cannot be explained by a change in TBT Window and was indeed a genuine TBT regression caused by a third party:

SpeedCurve Common Requests table comparing JS CPU Time between two tests, showing several third-party scripts using more CPU in Test Two, including Google Tag Manager rising from 70 ms to 149 ms and Sentry rising from 186 ms to 344 ms.
This is the other side of the diagnostic: when the TBT Window is stable, a TBT spike is much more likely to be real blocking work.

In dashboards, I would probably expose this as TBT and TBT Window. Tracking changes across both metrics in tandem can highlight genuine regressions versus a larger surface area.

This is no longer theoretical: synthetic tools can expose the TBT Window today, and I think they should. In fact, happily, after advising Matt on this exact topic, he added it to DebugBear which I believe makes them the first!

DebugBear lab test dashboard showing Total Blocking Time over time and a highlighted Total Blocking Time Window card reporting 4.42 seconds between FCP and TTI.
Showing the TBT Window alongside TBT turns this from a hidden implementation detail into useful debugging context.

None of this changes the definition of TBT, and none of it makes TBT any less useful, but it does give teams a better way to interpret any movement they are already seeing.

Closing Thoughts

TBT is still a useful lab metric. It is not a replacement for Interaction to Next Paint, and it is not a perfect model of real-user interactivity, but it remains a very practical way to spot main thread contention during page load.

Unfortunately, however, I feel it being bounded by the arbitrary definitions of TTI leaves a lot to be desired—entirely unrelated and unexpected network activity can completely tank otherwise healthy pages.

TTI may have left the Lighthouse score, but it has not entirely left your metrics. If you monitor TBT, keep an eye on your TBT Window as well. Otherwise, sooner or later, you will find yourself trying to find JavaScript that didn’t even change.

Citation

I’m making this article available under the same MIT License as the rest of this site, but if you quote, document, implement, or surface the term TBT Window in tooling, dashboards, talks, or written material, please cite this article as its canonical source:

Harry Roberts, ‘Front-End’s Missing Metric: The TBT Window’, CSS Wizardry, 1 June 2026, https://csswizardry.com/2026/06/front-ends-missing-metric-the-tbt-window/


Frequently Asked Questions

What is the TBT Window?

The TBT Window is the interval between First Contentful Paint and Time to Interactive during which Total Blocking Time counts the blocking portions of long tasks.

Why can TBT change when JavaScript has not changed?

TBT can change if FCP or TTI moves, because that changes the measurement window in which long tasks are counted.

Can genuine long-task regressions widen the TBT Window?

Yes. Because TTI depends on long-task quietness, more or later long tasks can push TTI later, widening the TBT Window and increasing TBT for legitimate reasons.

Is TTI still an important metric?

TTI is no longer part of the Lighthouse performance score, but it still matters for TBT because it defines the end of the TBT Window.

Why should synthetic tools show the TBT Window?

Plotting TBT Window alongside TBT helps developers tell the difference between more blocking work and a larger measurement window.