WebSockets vs. SSE : une question d'ordre des événements et de cohérence des données
Le choix entre WebSockets et Server-Sent Events (SSE) est souvent résumé à des critères de taille de payload ou de simplicité HTTP. Dans une analyse poussée, José Valim recadre le débat autour des garanties d'ordonnancement des messages et de la fiabilité de l'interface utilisateur. Utiliser deux flux distincts pour les lectures et les écritures expose en effet l'application à de sévères conditions de concurrence.
Fil « Comparatif des garanties d'ordre entre WebSockets et Server-Sent Events »
Le piège des requêtes stateless et des flux séparés
Dans la conception d'interfaces réactives ou d'applications envoyant du HTML directement depuis le serveur, l'approche combinant Fetch (l'API de requête HTTP du navigateur) pour les écritures et SSE (Server-Sent Events, protocole d'émission de flux HTTP unidirectionnel du serveur vers le client) est souvent recommandée. Ses partisans soulignent la réutilisation des connexions TCP (Transmission Control Protocol, protocole de transport réseau garanti) grâce au multiplexage. Cependant, cette vision omet un facteur essentiel : chaque requête via Fetch reste stateless (sans conservation d'état entre les appels).
À chaque action utilisateur, le serveur doit déchiffrer la session et recharger l'utilisateur depuis la base de données ou le cache. À l'inverse, une connexion WebSocket (protocole de communication bidirectionnelle en temps réel) est authentifiée une seule fois lors de son ouverture, gardant l'état en mémoire. Mais au-delà des performances et des payloads réduits, le véritable enjeu réside dans le respect de l'ordre d'exécution des événements.
Des conditions de concurrence invisibles en production
Lorsqu'une application utilise SSE pour recevoir les mises à jour et Fetch pour envoyer les modifications, deux flux de données distincts coexistent. En raison des latences réseau, des temps d'exécution du garbage collector (le ramasse-miettes de libération mémoire) ou du comportement des proxys, l'ordre de réception n'est pas garanti.
Imaginons une ressource possédant les étiquettes "erlang", "clojure" et "javascript". Si un utilisateur ajoute la métadonnée "elixir" pendant qu'un autre supprime "javascript", la réponse de la suppression transmise par Fetch peut arriver après le flux de mise à jour de l'ajout. L'interface graphique va alors afficher brièvement la nouvelle liste avant de la remplacer par une version obsolète. L'application ne respecte même pas le principe de cohérence éventuelle (eventual consistency, garantie que les copies de données convergent à terme si aucune modification n'intervient) : l'affichage reste corrompu tant qu'aucun nouvel événement n'est émis ou que l'utilisateur ne rafraîchit pas la page.
Les limites des contournements avec SSE et Fetch
Pour garantir l'ordre causal sans utiliser de canal bidirectionnel, les équipes doivent mettre en place des mécanismes complexes qui dégradent les performances :
- Attendre la propagation par SSE : la requête Fetch effectue l'écriture sans renvoyer de données, et le client attend la mise à jour globale via le flux SSE. Cela introduit une latence accrue en imposant un système de file d'attente entre serveurs.
- Invalidation systématique : le canal SSE sert uniquement à notifier qu'un changement a eu lieu, ce qui déclenche un rechargement par le client. Cette approche augmente considérablement la charge du serveur et exige d'ordonnancer les requêtes sur le client.
- Réordonnancement côté client : tenter de réaligner les événements hors séquence dans le navigateur s'avère très difficile, notamment lorsqu'une suppression arrive avant une création.
La bidirectionnalité des WebSockets comme solution native
Les WebSockets apportent une réponse structurelle à ce problème grâce à leur canal bidirectionnel. Les requêtes du client et les réponses du serveur empruntent exactement le même tuyau. L'ordre des événements est ainsi préservé nativement, sans nécessiter de routage interne complexe ni de files d'attente côté serveur.
// Exemple de client WebSocket préservant l'ordre des opérations et réponses
const socket = new WebSocket("wss://example.com/live");
socket.addEventListener("open", () => {
// L'authentification et l'état sont conservés en mémoire sur la connexion ouverte
socket.send(JSON.stringify({ type: "add_tag", tag: "elixir" }));
socket.send(JSON.stringify({ type: "remove_tag", tag: "javascript" }));
});
socket.addEventListener("message", (event) => {
// Les mises à jour du serveur arrivent séquentiellement sur le même flux
const state = JSON.parse(event.data);
renderTags(state.tags);
});
Avant de choisir entre WebSockets et SSE, l'architecture de l'application doit donc prioriser la gestion de l'ordre des événements pour prévenir toute désynchronisation des données à l'écran.