Node.js Internals

HTTP/2 & HTTP/3: Modern Protocols

Your modern website makes 150 HTTP requests per page. In HTTP/1.1, this is a disaster - the browser opens 6 TCP connections, each with a handshake delay of 300ms (mobile 4G). Requests are queued, one slow API blocks the loading of all others. The user sees a white screen for 4 seconds. You try workarounds: domain sharding, CSS sprites, inline base64 images - but this complicates deployment and breaks caching. HTTP/2 solves the problem fundamentally: one TCP handshake, multiplexing 150 requests in parallel, header compression. HTTP/3 goes further - eliminates TCP head-of-line blocking via QUIC, 0-RTT reconnect for mobile. It's not just a "new version of the protocol" - it's a rethinking of how the modern web works.

  • **Mobile web performance:** A user on 4G with 200ms latency opens an online store. HTTP/1.1: 6 connections × 200ms handshake = 1.2s until the first request, head-of-line blocking adds another 2s → total 3.2s white screen. HTTP/2: 1 connection × 200ms = 200ms, multiplexing 100 requests → 1s until full render. HTTP/3 + 0-RTT: returning user → 0ms handshake → 600ms total. Conversion increased by 15% after migrating to HTTP/2 + 103 Early Hints.
  • **Real-time dashboard with WebSocket + REST API:** The dashboard subscribes to WebSocket for live updates + makes 50 REST API requests to load data. HTTP/1.1: WebSocket occupies one of 6 connections → only 5 left for API → loading takes 10s. HTTP/2: WebSocket + 50 API requests go in parallel over a single multiplexed connection → loading takes 2s. HTTP/3: connection migration when switching Wi-Fi → mobile → WebSocket does not break, no reconnect lag.
  • **CDN for streaming video:** Video chunks (2s segments) are loaded via progressive download. HTTP/1.1: each chunk = new request, head-of-line blocking between chunks → buffering with slow segments. HTTP/2: chunks are multiplexed, prioritization through PRIORITY frames → critical chunks (beginning of the video) are loaded first. HTTP/3: packet loss on mobile does not block subsequent chunks (independent QUIC streams) → smooth playback on 4G.

Evolution of HTTP: from 1.1 to 3.0

**HTTP/1.1 is a protocol from 1999 that is still widely used.** But the web has changed radically: a modern page loads 100+ resources (JS, CSS, images, fonts, API calls), while HTTP/1.1 was designed for simple HTML documents with a couple of images. The main issue is **head-of-line blocking**: one slow request blocks all subsequent ones in the queue.

Imagine: a browser requests 6 files simultaneously over a single TCP connection. HTTP/1.1 requires that responses arrive **strictly in order**. If the first file (script.js) is delayed on the server for 2 seconds, the other 5 files wait, even though they are ready instantly. The solution in HTTP/1.1 is to open 6 parallel TCP connections (browsers limit to 6 per domain). But this is a workaround: each connection = TCP handshake (1 RTT) + TLS handshake (2 RTT) = 3 RTT delay before the first byte.

**Head-of-line blocking in HTTP/1.1:** Requests/responses are sequential. If response 1 is delayed, responses 2-6 wait, even if they are ready. **Workaround:** browsers open 6 TCP connections per domain. **Problem with workaround:** TCP slow start on each connection, high overhead on handshakes. **HTTP/2 solution:** multiplexing - all requests go in parallel over a single TCP connection.

**Chronology of HTTP Evolution:** HTTP/1.0 (1996) → one connection per request. HTTP/1.1 (1999) → keep-alive, pipelining (but doesn't work due to HOL blocking). HTTP/2 (2015) → multiplexing, binary protocol, header compression. HTTP/3 (2022) → QUIC instead of TCP, 0-RTT handshake, independent streams without HOL blocking at the transport level.

What is the main problem of HTTP/1.1 when loading 100 resources?

Multiplexing and binary framing

**HTTP/2 reinvents data transmission at a fundamental level.** Instead of text-based requests/responses, HTTP/2 uses **binary framing** - data is broken into small frames (DATA, HEADERS, PRIORITY), which are transmitted **in parallel across multiple streams** within a single TCP connection. Each stream is an independent request/response, and frames from different streams **interleave**.

**Binary Frame:** The minimal unit of data in HTTP/2. Consists of: frame header (9 bytes: length, type, flags, stream ID) + payload. **Frame Types:** HEADERS (request/response headers), DATA (body), PRIORITY (stream priority), RST_STREAM (cancellation), SETTINGS (connection settings), PING (keep-alive). **Stream:** A logical channel for a single request-response. Stream ID is odd for client-initiated, even for server push.

**Stream prioritization:** The client can specify the weight (weight 1-256) and dependency of streams. For example: CSS weight=200, JS weight=150, images weight=50 → the server will send CSS frames more frequently. Dependency: "do not send JS until CSS is loaded." In practice, prioritization is complex and often ignored by servers.

**HPACK - header compression:** HTTP/1.1 sends the same headers (User-Agent, Cookie) with each request. For 100 requests, this is ~50KB overhead. HTTP/2 uses HPACK: headers are compressed through Huffman encoding + indexing. The first request sends full headers → an index table is created. Subsequent requests: "headers as in index #5". Savings: ~80% of header size.

What allows HTTP/2 to avoid head-of-line blocking?

Server Push and HPACK compression

**Server Push is the server's ability to send resources to the client BEFORE the client requests them.** A classic scenario: the client requests `/index.html`. The server knows that the HTML references `style.css` and `app.js`. Instead of waiting for two additional requests, the server **pushes** the CSS and JS immediately with the HTML. Savings: 1 RTT for each pushed resource.

**How Server Push Works:** 1) The client requests `/index.html` (stream 1) 2) The server sends a PUSH_PROMISE frame: "I will send you `/style.css` via stream 2" 3) The server starts sending HEADERS + DATA for stream 2 BEFORE the client requests it 4) The browser receives the pushed resource and stores it in the cache 5) When the HTML is parsed and encounters `<link rel=stylesheet>`, the browser takes the CSS from the cache instead of making a new request.

**Problems with Server Push in practice:** 1) **Cache invalidation is hard** - the server does not know if the resource is already in the browser cache. It may push something that is already there (wasted bandwidth) 2) **Over-pushing** - the server pushes 10 files, but the user closes the page after 1 second (wasted CPU/network) 3) **Implementation complexity** - logic is needed to determine what to push. Due to these problems, **Server Push is practically not used** and was removed from Chrome in 2022. Instead, there is `103 Early Hints` (HTTP status code).

**HPACK details:** Dynamic table (4KB by default) + static table (61 predefined headers). Example: the first request sends `User-Agent: Mozilla/5.0...` (200 bytes) → index #15 in the table. Subsequent requests: just the number `15` instead of 200 bytes. Huffman encoding additionally compresses strings by ~30%. In total: headers are reduced by 5-10 times for typical web applications.

Why was Server Push removed from Chrome in 2022?

HTTP/3 and QUIC: The UDP Revolution

**HTTP/3 solves the last remaining issue of HTTP/2: head-of-line blocking at the TCP level.** HTTP/2 removed HOL blocking at the application level (through stream multiplexing), but TCP **still** requires packets to be delivered in order. If packet #5 is lost in the network, TCP blocks the delivery of packets #6, #7, #8 to the application until #5 is retransmitted. All HTTP/2 streams freeze due to a single lost TCP packet!

**QUIC (Quick UDP Internet Connections):** A transport protocol from Google, operates over UDP instead of TCP. **Key features:** 1) **Independent streams** - packet loss in stream 1 does not block stream 3 2) **0-RTT handshake** - reconnecting to the server takes 0 RTT (data is sent immediately) 3) **Built-in TLS 1.3** - encryption at the transport level 4) **Connection migration** - change of IP/port without breaking the connection (switching Wi-Fi → mobile) 5) **Better congestion control** - improved algorithms compared to TCP.

**0-RTT handshake magic:** Traditionally: TCP handshake (SYN, SYN-ACK, ACK = 1 RTT) + TLS handshake (ClientHello, ServerHello, keys = 1-2 RTT) = **2-3 RTT before the first byte of data**. QUIC: on the first connection - 1 RTT (combined crypto + transport handshake). On reconnection (resume) - **0 RTT**: the client sends data immediately with the first packet using saved keys. This is critical for mobile applications with frequent reconnections.

**Adoption of HTTP/3:** Google (all services), Facebook, Cloudflare, Fastly. ~30% of the top-10K sites support HTTP/3 (2024). Browsers: Chrome/Edge (stable), Firefox (stable), Safari (beta). Node.js: experimental support via flags, production-ready libraries are emerging (cloudflare/quiche bindings). Issues: UDP is blocked on some corporate firewalls → fallback to HTTP/2.

How does HTTP/3 (QUIC) solve TCP head-of-line blocking?

Migration and Practical Application

**Migration from HTTP/1.1 to HTTP/2 is not just about flipping a switch.** You need to rethink optimization patterns that were workarounds for HTTP/1.1 but are detrimental in HTTP/2. **HTTP/1.1 antipatterns that need to be removed:** 1) **Domain sharding** (assets1.cdn.com, assets2.cdn.com) - created to bypass the 6 connection limit. In HTTP/2, this slows down: multiplexing is lost, multiple TCP handshakes 2) **Sprite images** - combining icons into one file. In HTTP/2, it's cheaper to load 50 small icons in parallel 3) **Inlining CSS/JS in HTML** - increases HTML size, kills caching. HTTP/2 will load separate files in the same RTT.

**HTTP/2 Migration Checklist:** 1) **TLS is mandatory** - browsers require HTTPS for HTTP/2 (although the specification allows h2c - cleartext) 2) **Reverse proxy** - Nginx, Caddy, HAProxy support HTTP/2 out-of-the-box. A Node.js application can run on HTTP/1.1 behind a proxy 3) **Remove domain sharding** - one domain for all assets 4) **Split the bundle** - instead of 1MB app.js → 10 files of 100KB each (parallel loading + better caching) 5) **103 Early Hints** instead of Server Push 6) **Monitoring** - check that clients are actually using HTTP/2 via `req.httpVersion`.

**Performance comparison (typical website, 100 resources, 200ms RTT):** HTTP/1.1 (6 connections): ~12 RTT = 2.4s. HTTP/2 (1 connection, multiplexing): ~4 RTT = 800ms (3x faster). HTTP/3 (QUIC, 0-RTT resume): ~2 RTT = 400ms (6x faster for returning users). Real world: HTTP/2 provides a 10-30% improvement in median load time, HTTP/3 provides an additional 5-15% on mobile networks with packet loss.

HTTP/2 automatically speeds up any application without code changes

HTTP/2 requires a revision of optimizations - domain sharding, sprites, inlining are harmful. The architecture needs to be adapted.

HTTP/1.1 patterns (domain sharding, CSS sprites, inline assets) were workarounds for head-of-line blocking and the 6 connections limit. In HTTP/2, these "optimizations" become anti-patterns: domain sharding creates unnecessary TCP handshakes, sprites kill parallelism, inlining breaks caching. You need to split bundles, remove sharding, and use separate files - then HTTP/2 will have its full effect.

Why is domain sharding (assets1.cdn.com, assets2.cdn.com) an anti-pattern for HTTP/2?

Key Ideas

  • **HTTP/1.1 suffers from head-of-line blocking:** requests are processed sequentially, a slow response blocks the entire queue. The workaround (6 parallel TCP connections) is inefficient: each connection = 3 RTT handshake (TCP + TLS), TCP slow start for each. HTTP/2 solves this through binary multiplexing: all requests go in parallel streams over a single TCP connection.
  • **HTTP/2 optimizations:** HPACK header compression (80% savings), stream prioritization, Server Push (deprecated, replaced by 103 Early Hints). Migration requires removing HTTP/1.1 anti-patterns: domain sharding, CSS sprites, inlining. Best practice: split bundles, one domain, TLS mandatory, reverse proxy (Nginx) for HTTP/2 termination.
  • **HTTP/3 (QUIC) eliminates TCP head-of-line blocking:** operates over UDP, independent streams at the transport level. Packet loss in stream 1 does not block stream 3. Additional features: 0-RTT handshake (instant reconnect), connection migration (network switching without disconnection), built-in TLS 1.3. Adoption is growing: 30% of top sites, all major CDNs. Node.js support is experimental, production through libraries.

Related topics

HTTP/2 and HTTP/3 are related to all aspects of web architecture: from low-level networking to high-level optimization patterns.

  • TLS & Security — HTTP/2 requires TLS in browsers (ALPN negotiation), HTTP/3 embeds TLS 1.3 in QUIC. 0-RTT handshake requires understanding of replay attack mitigations.
  • Streams API — HTTP/2 streams in Node.js are implemented through Duplex streams. Response streaming is critical for Server Push and chunked transfer.
  • Performance & Profiling — Waterfall charts show HTTP/2 multiplexing vs HTTP/1.1 blocking. Connection timing (TTFB, DNS, TCP, TLS) is critical for optimization.

Вопросы для размышления

  • If a mobile 4G has 200ms RTT and 5% packet loss, how much faster will loading 100 resources be in HTTP/3 vs HTTP/2? Consider 0-RTT handshake and independent QUIC streams with packet loss.
  • Why was Server Push removed from Chrome, but 103 Early Hints works? What is the fundamental difference in approach - who makes the decision to load a resource?
  • Your API server is behind an Nginx reverse proxy. The client connects to Nginx via HTTP/2, and Nginx proxies requests to Node.js via HTTP/1.1. Does the client benefit from HTTP/2 multiplexing? Where does the bottleneck occur?

Связанные уроки

  • net-21-http-basics
HTTP/2 & HTTP/3: Modern Protocols

0

1

Sign In