HTTP/2 and HTTP/3

Know which performance advice the newer protocols made wrong, and why.

12of 18
≈ 11Min read
6Parts

What HTTP/1.1 could not do

One connection could carry one request at a time. A browser opened about six per origin and queued everything else behind them — so a page with sixty assets spent most of its life waiting in line. That single limitation produced a decade of workarounds: sprite sheets, concatenated bundles, inlined images, and domain sharding — splitting assets across cdn1, cdn2, cdn3 to get more parallel connections.

HTTP/2: one connection, many streams

Because a request is nearly free, most of the old advice inverts: do not shard domains (each one is another handshake), do not concatenate everything into one file (one changed byte invalidates the whole bundle), and do not inline images as data URIs (they cannot be cached separately and they bloat the document).

Smaller, well-cached files are now better than fewer large ones — up to a point: there is still per-request overhead, and a thousand modules is slower than fifty bundles.

HTTP/1.1HTTP/2
Requests in flight1 per connection, ~6 per originall of them, on one connection
Headersplain text, repeated in fullbinary, compressed (HPACK)
Encodingtextbinary frames
Cost of a small filea whole connection slota stream — nearly free

The blocking that stayed

HTTP/2 removed head-of-line blocking at the HTTP layer, but it all runs on one TCP connection — and TCP delivers bytes in order. A single lost packet stalls every stream on that connection until it is retransmitted. On a good network you never notice; on a lossy mobile one, HTTP/2 can be slower than HTTP/1.1 with six connections.

That is the problem HTTP/3 exists to fix

HTTP/3 runs over QUIC, which is built on UDP and tracks each stream separately: a lost packet stalls only its own stream. It also merges the transport and TLS handshakes into one round trip, and survives a change of network — your connection continues when the phone moves from wi-fi to mobile data.

Server push is gone, preload is not

HTTP/2 Server Push let a server send a file before it was asked for. It was removed from Chrome in 2022: servers pushed things the browser already had cached, and the waste outweighed the gain. <link rel="preload"> and 103 Early Hints do the useful part — telling the browser what to fetch, and letting it decide.

what replaced it
HTTP/1.1 103 Early Hints
Link: </assets/app.8f3d21.css>; rel=preload; as=style

<link rel="preload" href="/fonts/space.woff2" as="font" crossorigin>

Sharding had a limit even then

Splitting sixty files across two origins on HTTP/1.1 halves the queue and costs one extra handshake — a clear win. A third origin halves it again by less and costs another handshake, and somewhere around there the arithmetic turns. The lab below is that arithmetic; it is worth doing once, because it is the same trade-off as adding a font host or an analytics domain today.

What to actually do

  • Serve everything from one origin where you can; use preconnect for the one or two you cannot.
  • Bundle by change frequency, not into one file: vendor code and application code have different lifetimes.
  • Keep compression on (br where available), and keep the hashed-asset caching from the caching lesson.
  • Enable HTTP/3 if your CDN offers it — it costs nothing and helps exactly the users on the worst networks.

Practice