---
layout: post
title: "Front-End’s Missing Metric: The TBT Window"
date: 2026-06-01 11:30:00
categories: Web Development
main: "https://csswizardry.com/wp-content/uploads/2026/06/speedcurve-02.png"
meta: "The TBT Window is the FCP-to-TTI interval used to calculate Total Blocking Time. If FCP or TTI moves, TBT can change even when long tasks do not."
faq:
- question: "What is the TBT Window?"
answer: "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."
- question: "Why can TBT change when JavaScript has not changed?"
answer: "TBT can change if FCP or TTI moves, because that changes the measurement window in which long tasks are counted."
- question: "Can genuine long-task regressions widen the TBT Window?"
answer: "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."
- question: "Is TTI still an important metric?"
answer: "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."
- question: "Why should synthetic tools show the TBT Window?"
answer: "Plotting TBT Window alongside TBT helps developers tell the difference between more blocking work and a larger measurement window."
---
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](https://www.speedcurve.com/) 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](/contact/) 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](/2024/11/designing-and-evolving-a-new-performance-score/) I like to call
the _TBT Window_.
{% include promo.html %}
## A Quick Refresher
[Total Blocking Time](https://web.dev/articles/tbt/) is a lab metric intended to
quantify how unavailable the main thread is during page load. It looks at [long
tasks](https://w3c.github.io/longtasks/)—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](/inp-simulator/) 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](https://developer.chrome.com/docs/lighthouse/performance/lighthouse-total-blocking-time/).
This is key to understanding the context of the TBT metric itself.
[First Contentful Paint](https://web.dev/articles/fcp/) (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](https://web.dev/articles/tti/) (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](https://developer.chrome.com/blog/lighthouse-10-0/) 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:
> 0. Start at First Contentful Paint (FCP).
> 0. 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.
> 0. Search backwards for the last long task before the quiet window, stopping
> at FCP if no long tasks are found.
> 0. 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.
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
{% include cross-sell.html %}
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_.
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.
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:
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](/2022/03/optimising-largest-contentful-paint/) 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](/2024/08/blocking-render-why-whould-you-do-that/)
`