Comment diffuser des données vers le navigateur avec Fetch
La plupart des tutoriels sur l’API Fetch vous montrent le même schéma : appeler fetch(), attendre la réponse, appeler .json() ou .text(), terminé. Cela fonctionne bien pour les petites charges utiles. Mais lorsque votre serveur génère des données de manière progressive — pensez aux réponses d’IA, aux journaux en direct ou aux grands ensembles de données — attendre la réponse complète avant de toucher un seul octet pose un véritable problème.
La bonne nouvelle : l’API Fetch prend déjà en charge le streaming incrémental de données dans le navigateur. Voici comment l’utiliser.
Points clés à retenir
- Le
response.bodyde l’API Fetch expose unReadableStream, vous permettant de traiter les données morceau par morceau au fur et à mesure de leur arrivée plutôt que d’attendre la charge utile complète. - Utilisez
response.body.getReader()avec unTextDecoderpour une compatibilité navigateur optimale lors de la lecture de réponses en streaming. - Les morceaux réseau ne respectent pas les limites de messages — vous devez mettre en mémoire tampon et diviser vous-même les lignes incomplètes lors de l’analyse de formats structurés comme le JSON délimité par des sauts de ligne.
- Associez toujours les flux de longue durée à un
AbortControllerafin de pouvoir annuler proprement les requêtes lorsque les utilisateurs quittent la page.
Pourquoi le streaming de réponses avec l’API Fetch est important
Lorsque vous appelez response.json() ou response.text(), le navigateur doit recevoir l’intégralité du corps de la réponse avant de résoudre la promesse. Pour un fichier journal de 50 Mo ou un point de terminaison de complétion IA lent, cela signifie que votre application ne peut traiter ni afficher aucune partie de la réponse tant que le dernier octet n’est pas arrivé.
Le streaming vous permet de traiter les données au fur et à mesure de leur arrivée — affichant le premier morceau aux utilisateurs pendant que le reste est encore en transit. C’est une amélioration significative des performances perçues.
Comment fonctionne l’API Fetch avec ReadableStream
Chaque réponse fetch() expose un ReadableStream sur response.body. Au lieu d’attendre la charge utile complète, vous attachez un lecteur et récupérez les morceaux au fur et à mesure qu’ils arrivent du réseau.
L’approche la plus largement compatible est response.body.getReader() :
const response = await fetch('/api/stream')
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { value, done } = await reader.read()
if (done) break
console.log(decoder.decode(value, { stream: true }))
}
Chaque value est un Uint8Array d’octets bruts. TextDecoder convertit ces octets en chaîne de caractères. Passez { stream: true } pour que le décodeur gère correctement les caractères multi-octets qui pourraient être divisés entre les limites de morceaux.
Note sur l’itération asynchrone : Vous avez peut-être vu
for await (const chunk of response.body). Cette syntaxe est plus propre mais n’est pas prise en charge dans Safari à partir de la version 18.x, donc la bouclegetReader()ci-dessus est le choix le plus sûr pour la production. Consultez la compatibilité navigateur actuelle sur https://caniuse.com/wf-async-iterable-streams.
Décoder les flux de texte avec TextDecoderStream
Si vous préférez une approche de type pipeline, TextDecoderStream gère le décodage automatiquement :
const response = await fetch('/api/stream')
const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader()
while (true) {
const { value, done } = await reader.read()
if (done) break
console.log(value) // déjà une chaîne de caractères
}
C’est plus propre lors du chaînage de plusieurs étapes de transformation.
Discover how at OpenReplay.com.
Considérations pratiques pour le streaming dans le navigateur avec Fetch
Les limites de morceaux sont arbitraires. Les morceaux réseau ne s’alignent pas avec les lignes ou les messages. Si vous analysez du JSON délimité par des sauts de ligne ou des événements SSE, vous devez mettre en mémoire tampon les lignes incomplètes et diviser sur \n vous-même.
Les flux ne peuvent être consommés qu’une seule fois. Attacher un lecteur avec getReader() verrouille le flux à ce lecteur, et une fois que des données ont été lues, le corps devient perturbé et ne peut plus être consommé à nouveau. Si vous avez besoin du corps à deux endroits, appelez response.clone() avant la lecture :
const response = await fetch('/api/data')
const clone = response.clone()
// Lire l'original en tant que flux
const reader = response.body.getReader()
// Utiliser le clone normalement ailleurs
const text = await clone.text()
Annuler les flux avec AbortController. Les flux de longue durée doivent pouvoir être annulés — en particulier lorsque les utilisateurs quittent la page :
const controller = new AbortController()
const response = await fetch('/api/stream', {
signal: controller.signal
})
// Annuler si nécessaire
controller.abort()
Cela empêche le navigateur de continuer à recevoir des données que personne ne lit.
Conclusion
Le streaming dans le navigateur avec Fetch est bien pris en charge et pratique aujourd’hui. Le schéma de base est simple : obtenir un lecteur depuis response.body, boucler avec reader.read(), décoder les octets avec TextDecoder, et gérer les limites de morceaux dans votre propre tampon. Ajoutez un AbortController pour le nettoyage, et gardez à l’esprit que les corps de réponse ne peuvent être consommés qu’une seule fois lorsque vous avez besoin des données à plusieurs endroits. C’est tout ce dont vous avez besoin pour créer des expériences de données réactives et incrémentielles dans le navigateur.
FAQ
Le streaming Fetch fonctionne avec toute méthode HTTP qui retourne un corps de réponse, y compris POST, PUT et PATCH. Le ReadableStream sur response.body se comporte de manière identique quelle que soit la méthode utilisée. Le serveur doit simplement envoyer une réponse fragmentée ou en streaming pour que la lecture incrémentale ait un sens.
Vous devez maintenir un tampon de chaîne de caractères. Ajoutez chaque morceau décodé au tampon, puis divisez sur les caractères de saut de ligne. Traitez chaque ligne complète comme du JSON, et conservez le segment incomplet final dans le tampon pour le prochain morceau. Cela tient compte du fait que les morceaux réseau peuvent diviser un objet JSON sur deux lectures.
Oui. Vous pouvez consommer un point de terminaison SSE via le streaming Fetch en analysant manuellement le format text/event-stream à partir des morceaux. Cela vous donne plus de contrôle sur les en-têtes, l'authentification et les méthodes de requête par rapport à l'API EventSource, qui ne prend en charge que les requêtes GET et offre une personnalisation limitée des en-têtes.
Si la connexion est interrompue ou si le flux rencontre une erreur, la promesse retournée par reader.read() sera rejetée. Enveloppez votre boucle de lecture dans un bloc try-catch afin que votre application puisse gérer l'échec de manière élégante, notifier l'utilisateur ou réessayer la requête si approprié.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.