Pour faire suite à mon article précédent, j’ai voulu aborder un autre sujet sur lequel j’ai travaillé autour de cette même app de poker planning.
Oui, cette application de poker planning, poker.slashgear.dev est un peu anecdotique, j’en conviens. Mais c’est justement l’intérêt : c’est ma sandbox. Une app simple, avec des boutons, des interactions, plusieurs pages — parfaite pour expérimenter sans risque.
Et l’appli commence à avoir une vraie base d’utilisateurs réguliers : plus de 200 rooms créées et 1 500 votes. C’est franchement cool.
Les Server-Sent Events, mon choix technique
La synchronisation des votes entre les différents utilisateurs est gérée grâce aux Server-Sent Events (SSE)1.
Les SSE, c’est une connexion HTTP persistante unidirectionnelle : le serveur pousse des événements vers le client, sans que le client ait besoin de redemander. Par rapport aux WebSockets, c’est bien plus simple à mettre en place — pas de protocole de handshake, pas de connexion bidirectionnelle à gérer, et ça passe sans broncher à travers les proxies HTTP. Pour un cas d’usage comme le mien — diffuser un état partagé à plusieurs clients — c’est largement suffisant.
Si vous voulez voir comment c’est implémenté, le code est disponible :
- Côté serveur — avec
streamSSEde HonoJS, un keep-alive toutes les 30 secondes, et un broadcast via unMapde callbacks par room - Côté front — un simple
EventSourcedans un hook Preact
Voilà globalement comment les échanges se passent :
La présentation de M4DZ lors du meetup LyonJS de février 2026 est une excellente ressource pour creuser le sujet :
Les SSE, c’est élégant. Mais j’avais un doute.
La question qui gratte
Est-ce que les SSE tiennent vraiment la charge ? Est-ce que ça ne va pas flancher dès qu’il y a un peu de monde sur l’application ?
Il n’y a qu’une seule façon de le savoir : mesurer. C’est quelque chose que j’ai appris chez Bedrock Streaming, notamment grâce à Yann Verry.
J’avais déjà utilisé plusieurs outils pour ça dans le passé : Gatling et Artillery.
Gatling, avec sa DSL spécifique… franchement pas fan. Artillery, la configuration en YAML paraît séduisante au début, mais ça devient vite pénible à maintenir dès qu’on sort des scénarios simples.
C’est là que j’ai découvert k6.
k6, une solution à mon problème
k6 est un outil open source de tests de charge développé par Grafana Labs. Le principe : vous écrivez vos scénarios en JavaScript (ou TypeScript), et k6 se charge de les exécuter avec le niveau de charge que vous définissez — nombre d’utilisateurs virtuels, rampe de montée, durée.
Ce qui m’a convaincu, c’est son SDK TypeScript : une API claire, une excellente DX2, et la possibilité de modéliser des scénarios complexes avec les mêmes réflexes qu’on a en développement web. On code un test comme on coderait un script — pas de YAML, pas de DSL ésotérique.
J’ai mis en place cinq scénarios pour couvrir différentes situations :
| Scénario | Objectif | Charge simulée |
|---|---|---|
basic-workflow | Valider le flux nominal | 10–20 utilisateurs |
spike-test | Tester une montée brutale | 5 → 100 utilisateurs en 10s |
stress-test | Trouver le point de rupture | Jusqu’à 1 000 utilisateurs |
realistic-sessions | Simuler de vraies sessions | 500 utilisateurs, 50–100 rooms |
sse-endurance | Tester la tenue des connexions longues | 1 000 connexions SSE pendant 20 min |
Quelques conseils tirés de cette expérience :
- Commencez par le flux nominal avant d’attaquer les tests de stress. Ça permet de valider les seuils de base (P953 < 500ms pour moi) et de détecter les régressions facilement.
- Les
thresholdssont vos meilleurs alliés : définissez des seuils d’erreur et de latence dès le départ. Si un scénario les dépasse, k6 sort avec un code d’erreur — parfait pour l’intégrer dans une CI. - Pour les SSE, il n’y a pas de support natif d’
EventSourcedans k6. J’ai simulé les connexions longues via des requêtes HTTP persistantes, ce qui reste représentatif pour mesurer la tenue en charge du serveur.
Ce que je n’ai pas encore exploré : l’intégration avec Grafana Cloud pour visualiser les métriques en temps réel, et le mode k6 browser qui permet de piloter un vrai Chromium pour simuler des interactions utilisateur côté front.
Pour illustrer la flexibilité de k6, voici trois exemples de scénarios sur un site fictif (il y a d’autres exemples sur la documentation officielle de k6) :
Scénario HTTP — flux nominal
Le cas le plus courant : simuler des utilisateurs qui naviguent sur un site et appellent une API.
import http from "k6/http";
import { sleep, check } from "k6";
export const options = {
stages: [
{ duration: "30s", target: 20 },
{ duration: "1m", target: 20 },
{ duration: "15s", target: 0 },
],
thresholds: {
http_req_failed: ["rate<0.01"],
http_req_duration: ["p(95)<500"],
},
};
export default function () {
// Créer une room
const create = http.post(
"https://example.com/api/rooms",
JSON.stringify({ name: `room-${__VU}` }),
{ headers: { "Content-Type": "application/json" } },
);
check(create, { "room créée": (r) => r.status === 201 });
const { code } = create.json();
// Voter
const vote = http.post(
`https://example.com/api/rooms/${code}/vote`,
JSON.stringify({ value: 5 }),
{ headers: { "Content-Type": "application/json" } },
);
check(vote, { "vote accepté": (r) => r.status === 200 });
sleep(1);
}
Scénario Browser — interactions réelles via Chromium
Le module k6/browser pilote un vrai Chromium : idéal pour mesurer les Core Web Vitals et tester les interactions UI sous charge.
import { browser } from "k6/browser";
import { check } from "k6";
export const options = {
scenarios: {
ui: {
executor: "constant-vus",
vus: 3,
duration: "30s",
options: {
browser: { type: "chromium" },
},
},
},
thresholds: {
browser_web_vital_lcp: ["p(75)<2500"],
browser_web_vital_fid: ["p(75)<100"],
},
};
export default async function () {
const page = await browser.newPage();
try {
await page.goto("https://example.com");
// Remplir le nom et rejoindre une room
await page.locator('input[placeholder="Votre nom"]').fill(`User ${__VU}`);
await page.locator('button[data-action="join"]').click();
await page.waitForSelector(".room-board");
check(page, {
"board affiché": () => page.locator(".room-board").isVisible(),
});
// Voter
await page.locator('.card[data-value="5"]').click();
} finally {
await page.close();
}
}
Scénario MQTT — charge sur un broker de messages
Via l’extension k6/x/mqtt, k6 peut aussi tester des brokers MQTT — utile pour les apps IoT ou les systèmes de messagerie temps réel.
import mqtt from "k6/x/mqtt";
import { check } from "k6";
import { sleep } from "k6";
export const options = {
vus: 10,
duration: "30s",
thresholds: {
checks: ["rate>0.99"],
},
};
export default function () {
const client = new mqtt.Client({
brokerAddr: "broker.example.com:1883",
clientId: `k6-vu-${__VU}-${__ITER}`,
});
client.connect();
check(client, { connecté: (c) => c.isConnected() });
// Publier un événement
client.publish(
"rooms/updates",
JSON.stringify({ vu: __VU, vote: 8, ts: Date.now() }),
/* qos */ 1,
/* retain */ false,
);
// S'abonner et attendre un message de retour
const msg = client.subscribe("rooms/ack", /* qos */ 1, /* timeout ms */ 2000);
check(msg, { "ack reçu": (m) => m !== null });
sleep(1);
client.disconnect();
}
Et vous ?
Vous avez déjà mis en place des tests de charge sur vos projets ?
En tout cas, de mon côté, le verdict est rassurant : l’appli tient bien la charge.
Footnotes
-
Server-Sent Events — mécanisme HTTP permettant au serveur de pousser des événements vers le client via une connexion persistante unidirectionnelle. ↩
-
DX (Developer Experience) — qualité de l’expérience de développement offerte par un outil ou une API. ↩
-
P95 (95e percentile) — valeur en dessous de laquelle se situent 95 % des mesures. Un P95 à 500ms signifie que 95 % des requêtes répondent en moins de 500ms. ↩