Attendre 8 secondes qu'un LLM finisse avant d'afficher quoi que ce soit, c'est une UX morte. Le streaming token par token change tout. Et côté serveur, pas besoin de WebSocket : Server-Sent Events suffit, en PHP natif.
Le principe
SSE est un flux HTTP texte avec un Content-Type: text/event-stream. Le serveur écrit des lignes data: ...\n\n et le navigateur les reçoit via EventSource. Unidirectionnel serveur→client, exactement ce qu'il faut pour du token streaming.
Le code serveur
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no'); // désactive le buffer nginx
while (ob_get_level() > 0) { ob_end_flush(); }
foreach (streamLlm($prompt) as $chunk) {
echo 'data: ' . json_encode(['t' => $chunk]) . "\n\n";
flush();
}
echo "event: done\ndata: {}\n\n";
flush();
Côté client
const es = new EventSource('/stream?q=' + encodeURIComponent(q));
es.onmessage = (e) => { output.textContent += JSON.parse(e.data).t; };
es.addEventListener('done', () => es.close());
Les trois pièges
- Buffering nginx/Apache : le header
X-Accel-Buffering: no(nginx) et désactivergzipsur cette route, sinon tout arrive d'un bloc. - Buffer PHP : vider tous les niveaux d'
ob_*etflush()après chaque chunk. - Timeout FPM : un stream long tient un worker FPM. Limiter la durée, prévoir un
max_execution_timeadapté, et idéalement isoler ces requêtes d'un pool FPM dédié.
Pas de dépendance, ~15 lignes serveur. Pour un chat IA, c'est le strict nécessaire et ça suffit en production tant que la concurrence reste raisonnable.