Skip to content

Commit 91e3345

Browse files
committed
Add TBT Window post
1 parent 3ebf5cc commit 91e3345

1 file changed

Lines changed: 348 additions & 0 deletions

File tree

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
---
2+
layout: post
3+
title: "Front-End’s Missing Metric: The TBT Window"
4+
date: 2026-06-01 11:30:00
5+
categories: Web Development
6+
main: "https://csswizardry.com/wp-content/uploads/2026/06/speedcurve-02.png"
7+
meta: "The TBT Window is the FCP-to-TTI interval used to calculate synthetic Total Blocking Time. If FCP or TTI moves, TBT can change even when long tasks do not."
8+
faq:
9+
- question: "What is the TBT Window?"
10+
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."
11+
- question: "Why can TBT change when JavaScript has not changed?"
12+
answer: "TBT can change if FCP or TTI moves, because that changes the measurement window in which long tasks are counted."
13+
- question: "Can genuine long-task regressions widen the TBT Window?"
14+
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."
15+
- question: "Why should synthetic tools show the TBT Window?"
16+
answer: "Plotting TBT Window alongside TBT helps developers tell the difference between more blocking work and a larger measurement window."
17+
---
18+
19+
An incident (and it _was_ an incident!) on a client project got me thinking
20+
about the metrics we use, and the importance of fully understanding their exact
21+
definitions. Bizarrely defined and often overlooked specs sent panic around
22+
a team, but ultimately shed light on a tangible gap in our front-end tooling.
23+
24+
For my client, from one [SpeedCurve](https://www.speedcurve.com/) synthetic test
25+
to the next, their Total Blocking Time (TBT) skyrocketed from **495 ms** to
26+
**5,789 ms**. That’s more than a tenfold increase! Naturally, it was all
27+
hands on deck to pin down the cause, and I was called in to help out.
28+
29+
What I found was both enlightening and calming, but it did shine a light on
30+
a gap in the way we tend to talk about modern web performance metrics.
31+
32+
This is the story behind a new metric I like to call the _TBT Window_.
33+
34+
{% include promo.html %}
35+
36+
## A Quick Refresher
37+
38+
[Total Blocking Time](https://web.dev/articles/tbt/) is a lab metric intended to
39+
quantify how unavailable the main thread is during page load. It looks at [long
40+
tasks](https://w3c.github.io/longtasks/)—work that occupies the UI thread for
41+
more than 50 ms—and counts only the blocking portion of each one.
42+
A 70 ms task contributes 20 ms to TBT; a 250 ms task contributes
43+
200 ms. The first 50 ms of any task is effectively tolerated, and
44+
anything beyond that is considered blocking time.
45+
46+
This much is probably familiar, but the part that is often overlooked is that
47+
TBT is not an unbounded sum of all blocking work in the trace: it is a sum of
48+
all the blocking time [between First Contentful Paint and Time to
49+
Interactive](https://developer.chrome.com/docs/lighthouse/performance/lighthouse-total-blocking-time/).
50+
This is key to understanding the context of the TBT metric itself.
51+
52+
[First Contentful Paint](https://web.dev/articles/fcp/) (FCP) opens the window
53+
and is the first point in the page-load timeline at which the user can see
54+
something contentful on screen—the page is responding. [Time to
55+
Interactive](https://web.dev/articles/tti/) (TTI) closes the window and is the
56+
point at which the page is considered reliably interactive.
57+
58+
TBT is the blocking work that happens between those two moments.
59+
60+
This is slightly clumsy or even unexpected because TTI itself has largely
61+
disappeared from our day-to-day vocabulary. [Lighthouse 10 removed
62+
TTI](https://developer.chrome.com/blog/lighthouse-10-0/) from the performance
63+
score after a deprecation process that began in Lighthouse 8, and for perfectly
64+
sensible reasons: TTI was highly variable and <q>overly sensitive to outlier
65+
network requests and long tasks</q>. It is also seemingly arbitrarily defined
66+
and hard to reason about:
67+
68+
> 0. Start at First Contentful Paint (FCP).
69+
> 0. Search forward in time for a quiet window of at least five seconds, where
70+
> quiet window is defined as: no long tasks and no more than two in-flight
71+
> network GET requests.
72+
> 0. Search backwards for the last long task before the quiet window, stopping
73+
> at FCP if no long tasks are found.
74+
> 0. TTI is the end time of the last long task before the quiet window (or the
75+
> same value as FCP if no long tasks are found).
76+
77+
Wow.
78+
79+
<figure>
80+
<img src="{{ site.cloudinary }}/wp-content/uploads/2026/06/tti.svg" alt="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." width="750" height="443" loading="lazy">
81+
<figcaption>TTI is found by identifying the quiet window first, then walking back to the last long task before it.</figcaption>
82+
</figure>
83+
84+
The fact that TTI has been retired from the report, however, does not mean it
85+
has stopped influencing the numbers we still care about. If you are monitoring
86+
TBT, TTI still defines the far edge of the measurement window and changes in TTI
87+
scores can impact your TBT scores even in cases you might not expect.
88+
89+
Hopefully now you can see the problem. Per Google’s own documentation, TTI
90+
<q>proved overly sensitive to outlier network requests and long tasks</q>, yet
91+
if TTI also defines the upper bound for TBT, those same outliers can increase
92+
(or decrease) the surface area you’re being tested against.
93+
94+
**A degraded TTI score can degrade your TBT score.** You’re effectively still
95+
being measured against it. This is exactly what happened to my client.
96+
97+
## The TBT Window
98+
99+
{% include cross-sell.html %}
100+
101+
Once you think of TBT as a windowed metric, a few otherwise surprising
102+
behaviours become much easier to explain. The TBT Window is the interval between
103+
FCP and TTI during which TBT is allowed to count blocking work. If FCP moves
104+
earlier, the TBT Window opens earlier, so long tasks that used to sit before FCP
105+
can suddenly fall inside the window. If FCP moves later, the opposite can
106+
happen: long tasks can fall out of scope without having been removed from the
107+
page.
108+
109+
The same is true at the other end. If TTI moves earlier, the window closes
110+
sooner and late long tasks may stop contributing to TBT. If TTI moves later,
111+
the window stays open for longer and more of the tail of the page load can
112+
suddenly become countable.
113+
114+
This is the important bit: in all of these cases, the long tasks themselves may
115+
not have changed in any meaningful way. What changed was the amount of timeline
116+
that TBT was allowed to inspect. That moving interval is what I mean by the _TBT
117+
Window_.
118+
119+
<figure>
120+
<img src="{{ site.cloudinary }}/wp-content/uploads/2026/06/speedcurve.png" alt="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." width="1500" height="894" loading="lazy">
121+
<figcaption>The clue is that both lines move together: TBT rose as the end of its measurement window moved. <a href="/wp-content/uploads/2026/06/speedcurve-full.png">View full size/quality (416KB)</a></figcaption>
122+
</figure>
123+
124+
In my client’s case, their initial instinct was entirely understandable: a TBT
125+
regression looks like a JavaScript regression, and the SpeedCurve chart even had
126+
the word _JS_ in it, so the first place the team looked was their bundles. They
127+
checked for new packages, third-party changes, tag manager updates, and any new
128+
hydration or framework cost that might have slipped into the release.
129+
130+
It was a sensible line of investigation, but it turned nothing up and didn’t fit
131+
the facts. There had been no meaningful JavaScript-facing change in the
132+
timeframe they were looking at, and the waterfalls told a much more interesting
133+
story. There were plenty of late long tasks, but they had not materially changed
134+
between the before and after traces. What _had_ changed was TTI.
135+
136+
In the waterfalls below, I have annotated the five-second quiet window in yellow
137+
and worked back to TTI which is annotated as a vertical pink line. Note that,
138+
for the most part, both waterfalls share identical hallmarks.
139+
140+
<figure>
141+
<img src="{{ site.cloudinary }}/wp-content/uploads/2026/06/wpt-01.png" alt="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." width="1500" height="1043" loading="lazy">
142+
<figcaption>The yellow quiet window is much later after the change, pushing TTI across almost the full waterfall.</figcaption>
143+
</figure>
144+
145+
Next, let’s look specifically at long and blocking tasks. Again, the hallmarks
146+
of main thread blocking activity remain mostly unchanged, but the sheer surface
147+
area being measured is substantially larger:
148+
149+
<figure>
150+
<img src="{{ site.cloudinary }}/wp-content/uploads/2026/06/wpt-02.png" alt="Zoomed WebPageTest main-thread comparison showing broadly similar blocking activity in both traces, but a much longer measured TBT Window after the regression." width="1500" height="489" loading="lazy">
151+
<figcaption>The work did not suddenly appear; the metric simply had much more of the timeline available to count.</figcaption>
152+
</figure>
153+
154+
Before the regression, the page was considered interactive before a lot of that
155+
late activity mattered. After the regression, TTI had moved out toward 20
156+
seconds, which meant the TBT Window stretched much further across the waterfall
157+
and pulled previously out-of-scope work into the metric.
158+
159+
The team did have a TBT regression in terms of pure numbers, but they did not
160+
have the JavaScript regression they thought they had. This is a very important
161+
distinction because those two interpretations lead to entirely different
162+
investigations. One sends you into bundles, dependencies, hydration, and third
163+
parties; the other sends you into the definition of the metric and the
164+
conditions that moved its boundaries.
165+
166+
Without the TBT Window in mind, you can end up trying to optimise code that did
167+
not cause the change. The practical lesson I took from this was simple: if TBT
168+
skyrockets for no obvious JavaScript reason, check the network.
169+
170+
## The LCP Improvement That Tanked TBT
171+
172+
The interesting part of this story is what moved TTI in the first place. We had
173+
been working on LCP, and had added a `preload` for the LCP image on the previous
174+
day. This is exactly the kind of thing I like to see: identify the important
175+
image, get it discovered earlier, help the browser fetch it sooner, improve the
176+
user-visible experience. And it worked: the [LCP
177+
optimisation](/2022/03/optimising-largest-contentful-paint/) was doing what it
178+
was supposed to do.
179+
180+
But, unfortunately, it also changed the shape of the network. TTI’s calculation
181+
does not only care about long tasks; it also looks for a quiet window of at
182+
least five seconds, where quiet means no long tasks and **no more than two
183+
in-flight network `GET` requests**. By introducing just one additional request
184+
via `preload`, the page suddenly fell foul of the network quiet-window
185+
condition. TTI moved later, the TBT Window became much larger, and suddenly long
186+
tasks that had previously sat outside the measurement window were being
187+
included.
188+
189+
This is the sort of thing that makes performance work so interesting and, if we
190+
are honest, sometimes quite annoying. A perfectly sensible optimisation for one
191+
metric made a completely different metric look catastrophically worse. More
192+
specifically, an optimisation intended to improve LCP caused TTI to move later,
193+
which in turn caused TBT to include more of the existing long-task tail.
194+
195+
Two metrics that are, from a user-experience point of view, concerned with
196+
different things became highly dependent on each other through the small print
197+
of their specs.
198+
199+
This is not necessarily a criticism of Lighthouse, SpeedCurve, WebPageTest, or
200+
the metrics themselves (though I do wish TTI would be fully sunset, even as
201+
a windowing metric). It is a reminder that our tooling is doing exactly what it
202+
says on the tin, but we need to read the tin.
203+
204+
## Why TTI Can Move
205+
206+
The weird thing about TTI is that it sounds more intuitive than it actually is.
207+
The name makes it feel like a direct measure of when the page becomes usable,
208+
but the calculation is far more fiddly and esoteric than that. As above, TTI is
209+
jointly dependent on long tasks and network activity.
210+
211+
The latter is the part of TTI that I have never liked. The intent is probably
212+
sensible: a page that is still fetching resources may not be as settled as one
213+
that has gone quiet, but the threshold itself—no more than two in-flight `GET`
214+
requests for five seconds—is inevitably arbitrary, and allows network behaviour
215+
to influence a metric that many people read as interactivity. Frustratingly, in
216+
a world of tag managers, tracking, ads, lazy loading, and more, forensic control
217+
over network activity is seldom within developers’ reach.
218+
219+
That means TTI can move because of long tasks, but it can also move because the
220+
page fails to reach that required network quietness. This is precisely what
221+
happened here—a network optimisation changed the network profile, TTI moved,
222+
and the TBT Window grew.
223+
224+
The long tasks did not get worse, they just became visible to the metric.
225+
226+
## Causality Can Run Both Ways
227+
228+
One thing worth being clear about: an expanded TBT Window does not automatically
229+
mean the TBT regression is a false alarm. Because TTI is calculated from both
230+
network quietness and long-task quietness, a genuine increase in blocking work
231+
can itself push TTI later. Add a large task at 12 seconds, or move a cluster of
232+
tasks further down the trace, and the browser may no longer find its quiet
233+
window where it used to. TTI moves out, the TBT Window grows, and TBT rises for
234+
two reasons at once: there _is_ more blocking work _and_ the metric now has more
235+
timeline in which to count it.
236+
237+
This is a significant distinction. The TBT Window is not a way to explain away
238+
TBT, but a way to avoid guessing. If the window grows because FCP or network
239+
quietness changed, you may be looking at a windowing artefact; if the window
240+
grows because true long-task activity got worse, you have a real TBT regression
241+
with a larger surface area. Either way, plotting the window gives you the
242+
context needed to tell those scenarios apart.
243+
244+
## How We Fixed It
245+
246+
We didn’t. It wasn’t a regression in any real, user-facing sense and we also had
247+
next to no control over the root cause. Reverting the `preload` change would
248+
have improved the numbers, but it would have materially regressed the user
249+
experience.
250+
251+
Instead, I advised that the client put a secondary lever in place and only treat
252+
TBT as a regression if it moves independently (or at a faster rate) than TTI.
253+
All TBT charts now contain a matching TTI score that needs to diverge beyond
254+
a certain threshold before we act. If you’re monitoring TBT, I’d suggest you do
255+
the same.
256+
257+
{% include promo.html %}
258+
259+
{% comment %}
260+
## TBT on csswizardry.com
261+
262+
Since I implemented [Speculation
263+
Rules](/2024/12/a-layered-approach-to-speculation-rules/) on this site, my TBT
264+
has been much harder to keep under control. And while that doesn’t bother me, it
265+
again shines a light on the problem of underpinning TBT with an esoteric metric
266+
like TTI.
267+
268+
Speculation Rules work to fetch potential subsequent pages in the background
269+
during the browser’s idle time. This leads to a long tail of requests.
270+
{% endcomment %}
271+
272+
## Make the Window Visible
273+
274+
This is where I think synthetic tooling could help developers enormously.
275+
Anywhere that plots TBT should also plot the TBT Window alongside it. Not as
276+
a new score per se, but as a sanity check or control.
277+
278+
The data is already there. If the tool knows FCP and TTI, and it has used them
279+
to calculate TBT, it can also show the size of the window between them. That
280+
single extra line on a chart would make a lot of investigations much easier to
281+
reason about. Tools could even correlate window growth with TBT growth and alert
282+
teams to potential false flags and red herrings:
283+
284+
> **N.B.** TBT Window increased by 37% between these two tests—TBT scores may be
285+
> misleading.
286+
287+
In the interim, if you’re already rolling your own dashboards like you can in
288+
SpeedCurve, you just need to plot each of First Contentful Paint, Total Blocking
289+
Time, and Time to Interactive on the same time series:
290+
291+
<figure>
292+
<img src="{{ site.cloudinary }}/wp-content/uploads/2026/06/speedcurve-02.png" alt="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." width="1500" height="540" loading="lazy">
293+
<figcaption>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.</figcaption>
294+
</figure>
295+
296+
If TBT goes up and the TBT Window stays flat, you are probably looking at
297+
a genuine increase in blocking work; if TBT goes up and the TBT Window expands
298+
at the same time, you know to ask if or why FCP or TTI moved before assuming
299+
blocking time actually got worse.
300+
301+
Likewise, if TBT improves while the window shrinks, you should be careful about
302+
celebrating too quickly: you may have removed blocking work, or you may simply
303+
have stopped counting some of it.
304+
305+
In the screenshot above, the TBT spike around 19–20 May cannot be explained by
306+
a change in TBT Window and was indeed a genuine TBT regression caused by a third
307+
party:
308+
309+
<figure>
310+
<img src="{{ site.cloudinary }}/wp-content/uploads/2026/06/speedcurve-03.png" alt="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." width="1500" height="546" loading="lazy">
311+
<figcaption>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.</figcaption>
312+
</figure>
313+
314+
In dashboards, I would probably expose this as TBT and TBT Window. Tracking
315+
changes across both metrics in tandem can highlight genuine regressions versus
316+
a larger surface area.
317+
318+
This is no longer theoretical: synthetic tools can expose the TBT Window today,
319+
and I think they should. In fact, happily, after advising
320+
[Matt](https://www.mattzeunert.com/) on this exact topic, he added it to
321+
DebugBear which I believe makes them the first!
322+
323+
<figure>
324+
<img src="{{ site.cloudinary }}/wp-content/uploads/2026/06/debugbear.png" alt="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." width="1500" height="908" loading="lazy">
325+
<figcaption>Showing the TBT Window alongside TBT turns this from a hidden implementation detail into useful debugging context.</figcaption>
326+
</figure>
327+
328+
None of this changes the definition of TBT, and none of it makes TBT any less
329+
useful, but it does give teams a better way to interpret any movement they are
330+
already seeing.
331+
332+
{% include cross-sell.html %}
333+
334+
## Closing Thoughts
335+
336+
TBT is still a useful lab metric. It is not a replacement for [Interaction to
337+
Next Paint](https://web.dev/articles/inp/), and it is not a perfect model of
338+
real-user interactivity, but it remains a very practical way to spot main-thread
339+
contention during page load.
340+
341+
Unfortunately, however, I feel it being bounded by the arbitrary definitions of
342+
TTI leaves a lot to be desired—entirely unrelated and unexpected network
343+
activity can completely tank otherwise healthy pages.
344+
345+
TTI may have left the Lighthouse score, but it has not entirely left your
346+
metrics. If you monitor synthetic TBT, keep an eye on your TBT Window as well.
347+
Otherwise, sooner or later, you will find yourself trying to find JavaScript
348+
that didn’t even change.

0 commit comments

Comments
 (0)