Frontend Labs
⋯

80. Station walkthrough: from URL to bytes

Your page's real timeline

Navigation Timing (§25) records every phase of loading the page you are on. The station turns it into an animated bar chart:

const n = performance.getEntriesByType("navigation")[0];
const phases = [
  ["Redirects", n.redirectEnd - n.redirectStart],
  ["Service worker / cache", n.domainLookupStart - n.fetchStart],
  ["DNS lookup", n.domainLookupEnd - n.domainLookupStart],
  ["TCP connect", (n.secureConnectionStart || n.connectEnd) - n.connectStart],
  ["TLS handshake", n.secureConnectionStart ? n.connectEnd - n.secureConnectionStart : 0],
  ["Request → first byte", n.responseStart - n.requestStart],
  ["Download HTML", n.responseEnd - n.responseStart],
  ["Parse → DOMContentLoaded", n.domContentLoadedEventEnd - n.responseEnd],
];

On localhost DNS and TCP take about 0 ms, and TLS takes under 1 ms. The note under the chart explains that on a real site those phases are often 50–300 ms each, which is the whole argument for connection reuse, preconnect and HTTP/3.

The experiment: 18 requests at once

The station fires N parallel fetches at /api/delay?ms=600, which waits 600 ms on the server. It then draws a waterfall from the browser's own Resource Timing entries:

await Promise.all(Array.from({ length: n }, (_, i) =>
  fetch(`/api/delay?ms=${delay}&id=${i}&run=${run}`, { cache: "no-store" }).then((r) => r.json())));
const entries = performance.getEntriesByType("resource").filter((e) => e.name.includes(`run=${run}`));
// per request: queued/stalled = requestStart - startTime, TTFB = responseStart - requestStart,
// download = responseEnd - responseStart, protocol = e.nextHopProtocol, server time = e.serverTiming[0]

The result over each protocol (real runs, 18 requests, 600 ms each):

HTTP/1.1 (:8088) HTTP/2 (:8443)
Wall time ~1.8 s: three waves ~608 ms: one burst
Connections 6 (the per-origin limit) 1, with streams multiplexed
Waterfall a staircase with grey "queued" segments 18 bars that start together

The server adds a Server-Timing header, so the waterfall can show "server time 601 ms" without any extra API, the same way you'd expose database time to DevTools in production. The lane view guesses connection reuse by assigning each request to the first connection that was free when it started. For HTTP/2 there is only one lane.

TLS 1.3 vs 1.2, step by step

The handshake animation plays the real message sequence. TLS 1.3 sends its key share in the very first message, so the handshake finishes in one round trip, and the certificate travels encrypted. TLS 1.2 needs two round trips and sends the certificate in the clear. The station's "What did we negotiate?" panel asks the server what it actually saw:

// server.mjs — /api/whoami
function connectionInfo(req) {
  const socket = req.stream?.session?.socket ?? req.socket;   // h2: the session's TLS socket
  return {
    httpVersion: req.httpVersion,            // "2.0" or "1.1"
    alpn: socket.alpnProtocol || null,       // "h2"
    tls: socket.getProtocol?.() ?? null,     // "TLSv1.3"
    cipher: socket.getCipher?.()?.name ?? null,   // e.g. "TLS_AES_256_GCM_SHA384"
    headers: req.headers,                    // includes :method/:path pseudo-headers, sec-fetch-*, priority
  };
}

You see the HTTP/2 pseudo-headers (:method, :authority, :path), Fetch Metadata (sec-fetch-site, which §67 uses for CSRF), and priority: u=1, i, the RFC 9218 priority signal browsers send with each request.

Generated from real-life-JS-guide.md · 8 Oct 2026 16:36