HTTP en profondeur - 16 - HTTP/2 : multiplexage, compression et la fin des hacks

HTTP/2 resout les problèmes de HTTP/1.1 avec le multiplexage, HPACK et le framing binaire.

16 - HTTP/2 : multiplexage, compression et la fin des hacks

Ce que tu vas apprendre

  • Pourquoi HTTP/1.1 atteint ses limites
  • Le framing binaire de HTTP/2
  • Le multiplexage et pourquoi c'est revolutionnaire
  • HPACK et la compression des headers
  • Server Push et pourquoi ca a rate

Prerequisites

  • 15 - Proxy
  • Comprendre les bases de HTTP/1.1 (requête/réponse, headers)

Pendant des annees, on a construit des hacks au-dessus de HTTP/1.1. Sprites CSS pour combiner les images. Concatenation de fichiers JavaScript. Domain sharding pour ouvrir plus de connexions. Inline de CSS critique. Tout ca parce que le protocole avait un défaut fondamental. HTTP/2 a rendu la plupart de ces techniques obsolètes. Et pourtant, je vois encore des devs qui les appliquent par reflexe.

Le problème de HTTP/1.1

HTTP/1.1 fonctionne en mode requête-réponse sequentiel. Tu envoies une requête, tu attends la réponse, puis tu envoies la suivante. Sur une meme connexion TCP.

Le problème s'appelle le head-of-line blocking. Si la première requête est lente (une grosse image), toutes les suivantes attendent derrière. Meme si le serveur pourrait répondre instantanement aux autres.

La solution de HTTP/1.1 ? Ouvrir plusieurs connexions en parallèle. Les navigateurs limitent ca a 6 connexions par domaine. D'ou le hack du domain sharding : tu servais tes images depuis img1.example.com, img2.example.com, etc. pour multiplier les connexions.

Ca marchait. Mais chaque connexion TCP a un coût : handshake, slow start, mémoire. Six connexions c'est six fois le coût.

Le framing binaire

HTTP/1.1 est un protocole texte. Les headers sont du texte brut, separes par des retours a la ligne. Facile a debugger avec telnet, mais inefficace a parser.

HTTP/2 passe au binaire. Chaque communication est decoupee en frames :

+-----------------------------------------------+
|                Length (24 bits)                |
+---------------+---------------+---------------+
|  Type (8)     |  Flags (8)    |
+-+-------------+---------------+---------------+
|R|          Stream Identifier (31 bits)        |
+=+=============================================+
|                Frame Payload                  |
+-----------------------------------------------+

Les types de frames principaux :

  • HEADERS : les headers HTTP (compresses)
  • DATA : le corps de la réponse
  • SETTINGS : configuration de la connexion
  • WINDOW_UPDATE : contrôle de flux
  • PUSH_PROMISE : server push (on y revient)
  • RST_STREAM : annuler un stream sans fermer la connexion

Le binaire est plus compact et beaucoup plus rapide a parser. Par contre, tu ne peux plus debugger avec telnet. C'est le prix a payer.

Le multiplexage

C'est LA feature de HTTP/2. Sur une seule connexion TCP, tu peux avoir des dizaines de requêtes et réponses en parallèle. Chaque échange a un identifiant de stream.

Connexion TCP unique
  |
  |-- Stream 1 : GET /index.html
  |-- Stream 3 : GET /style.css
  |-- Stream 5 : GET /app.js
  |-- Stream 7 : GET /logo.png
  |
  (tous en parallele, entrelaces frame par frame)

Le serveur peut envoyer des frames de style.css et app.js en alternance. Plus de head-of-line blocking au niveau HTTP. Si un fichier est pret, il part immédiatement, sans attendre les autres.

Résultats concrets : plus besoin de domain sharding. Plus besoin de concatener tous tes JS en un seul bundle gigantesque. Plus besoin de sprites CSS. Le protocole gere le parallélisme nativement.

HPACK : compression des headers

En HTTP/1.1, chaque requête envoie tous ses headers. Cookie, User-Agent, Accept, Accept-Language... Ca fait facilement 500 octets a 2 Ko par requête. Multiplie par 50 requêtes pour charger une page. Ca fait du volume.

HPACK resout ca avec deux mecanismes :

Table statique : les 61 headers les plus courants ont un index fixe. method: GET c'est l'index 2. Un seul octet au lieu de 11.

Table dynamique : le client et le serveur maintiennent une table partagee des headers deja vus. Si tu envoies le meme cookie a chaque requête, seul l'index est transmis apres la première fois.

Huffman encoding : les valeurs sont compressees avec un codage de Huffman optimise pour le texte HTTP.

Résultat : 80 a 90% de réduction sur les headers. Sur une API qui fait beaucoup de petites requêtes, le gain est énorme.

Server Push : la bonne idee qui a mal tourne

L'idee etait seduisante : le serveur sait que quand tu demandes index.html, tu vas aussi avoir besoin de style.css et app.js. Alors il les envoie proactivement, sans attendre que le navigateur les demande.

Client: GET /index.html
Server: voici index.html
Server: tiens, voici aussi style.css (PUSH_PROMISE)
Server: et app.js tant qu'on y est (PUSH_PROMISE)

En pratique, ca a ete un desastre :

  • Le serveur pousse des ressources que le navigateur a deja en cache
  • Pas de mecanisme fiable pour annuler un push en cours
  • La logique de "quoi pousser" est difficile a maintenir cote serveur
  • Les CDN ont du mal a implementer ca correctement

Chrome a retire le support de Server Push en 2022. La fonctionnalité existe toujours dans la spec, mais personne ne l'utilise. Les Early Hints (103) et le preloading font mieux le job, avec paltemps.fr qui utilise du preload classique plutot que du push.

Prioritisation des streams

HTTP/2 permet au client de dire au serveur quels streams sont prioritaires. Le CSS critique avant les images de fond, par exemple.

En pratique, chaque navigateur implementait la prioritisation differemment. Les serveurs ne la respectaient pas toujours. HTTP/3 a repris le sujet avec un modèle simplifie (RFC 9218).

Comment activer HTTP/2

Bonne nouvelle : si tu utilises un reverse proxy moderne, HTTP/2 est probablement deja actif.

Caddy : HTTP/2 est actif par défaut. Rien a faire.

Nginx :

nginxserver {
    listen 443 ssl http2;
    # ...
}

Node.js (natif) :

javascriptconst http2 = require("http2");
const fs = require("fs");

const server = http2.createSecureServer({
  key: fs.readFileSync("key.pem"),
  cert: fs.readFileSync("cert.pem"),
});

server.on("stream", (stream, headers) => {
  stream.respond({ ":status": 200 });
  stream.end("Hello HTTP/2");
});

server.listen(8443);

Pour vérifier que ton site utilise HTTP/2 :

bashcurl -I --http2 https://example.com

Si la réponse commence par HTTP/2 200, c'est bon.

Résumé

  • HTTP/1.1 souffre du head-of-line blocking et limite a 6 connexions par domaine
  • HTTP/2 utilise un framing binaire et le multiplexage sur une seule connexion TCP
  • HPACK compresse les headers de 80-90%
  • Server Push etait une bonne idee mais a echoue en pratique
  • HTTP/2 est actif par défaut sur la plupart des reverse proxies modernes
  • Les hacks HTTP/1.1 (sprites, concat, domain sharding) sont obsolètes

Article précédent : 15 - Proxy Article suivant : 17 - HTTP/3 et QUIC

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.