Perfnode · field instrument 01

How much HTML fits in your first round trip?

You've probably come across the advice that the first 14kB of your HTML are special. Get your critical references in there and the browser finds them straight away. Put them after and they cost you an extra round trip.

This page checks that against your own connection. It loads a row of documents from a server in Singapore. They're all the same size and they're byte-for-byte identical apart from one thing: how far into the markup a stylesheet is referenced. First one at , last one at . All it measures is how long your browser takes to start fetching that stylesheet.

If the initial congestion window is doing what everyone says it does, you won't get a smooth curve. You'll get a flat line, a jump of one round trip, and another flat line.

Not run yet
How long the browser takes to find the stylesheet, by how deep it sits

Nothing measured yet

Each document sits on its own hostname, and the run loads every one of them several times over, throwing away anything that arrived on a connection that was already warm. Give it a minute.

Now move the window

Everything above came off a box running Linux's default initial window of ten packets. This second run is the same documents at the same offsets, against a second droplet that is identical in every respect except one line on its default route: initcwnd 30.

If the window is what draws the boundary, the step has to move. There is nothing else left for it to depend on.

Not run yet

Not run yet

Both curves land on the same chart above.

How it works

Every document is the same size, no exceptions. The padding is random base64 sitting inside an HTML comment, so there's nothing for a compressor to get its teeth into, and nginx has gzip off anyway. The generator refuses to build if a probe lands so much as one byte away from its target offset. What moves between documents is the offset and nothing else. Both servers get the same generated files, byte for byte.

The probe itself is <link rel="stylesheet" href="/probe.css">. Chrome fetches stylesheets at Highest priority, and the preload scanner picks them up as soon as it sees them rather than waiting for the parser — so the fetch starts on discovery, which is the thing being timed. No media attribute on purpose: CSS whose media doesn't match isn't fetched by the scanner at all. The number recorded is resource.startTime − navigation.responseStart.

Each offset lives on its own hostname with its own certificate. RFC 9113 §9.1.1 lets a client reuse an existing connection for a different hostname provided the origin is authoritative for it — same IP, and a certificate valid for the name in the URI. Both conditions, which is exactly what a wildcard certificate would have satisfied.

The run loads each document in a hidden iframe, shuffles the order on every pass, and keeps going until the spread settles down or it runs out of rounds. keepalive_timeout is short on the server, and the shuffle means every other hostname gets visited before it comes back round — plenty of time for the socket to have gone away.

Not everything counts. A sample gets dropped if connectEnd === connectStart, which per Navigation Timing means the load reused a persistent connection; same if it wasn't HTTP/2, or if the transfer size suggests something compressed it on the way. However many that was, it's in the status line.

Results are in round trips rather than milliseconds, each sample divided by that visitor's own TCP handshake time. A step of 1.0 is one extra round trip whether you're 15ms from Singapore or 300.

What went wrong on the way here

Giving each offset its own hostname turned out not to be enough on its own — hence the separate certificate for every one of them rather than a single wildcard. Let's Encrypt gives you fifty a week, which feels generous right up to the moment you add a second server and need another eleven.

Every iframe load gets a random query string on the end of the URL. The response already says no-store, so in principle that's belt and braces — but a cached response anywhere between the server and the parser would report a discovery time of almost nothing, which is indistinguishable from a probe sitting inside the first flight. Not the sort of failure you'd catch by looking at the chart, so it's easier to make every request unique and not think about it again.