W3docs

L'API Streams de JavaScript

Apprenez l'API Streams de JavaScript — lisez des données progressivement avec ReadableStream, écrivez avec WritableStream, transformez avec TransformStream et composez des pipelines efficaces pour le traitement de grandes quantités de données.

L'API Streams vous permet de traiter des données en petits morceaux au fur et à mesure de leur arrivée, au lieu de tout charger en mémoire d'un coup. C'est indispensable pour travailler avec de gros fichiers, des réponses réseau lentes et des données en temps réel : vous pouvez commencer à traiter les premiers octets pendant que le reste est encore en transit, sans jamais avoir à conserver toute la charge utile en mémoire.

L'API repose sur trois types fondamentaux. Un ReadableStream est une source depuis laquelle vous lisez des données. Un WritableStream est un collecteur vers lequel vous envoyez des données. Un TransformStream se place entre les deux, recevant des morceaux d'un côté et émettant des morceaux modifiés de l'autre. Une fois ces trois types compris, vous pouvez les assembler en pipelines efficaces.

Lire un Stream

La façon la plus courante d'obtenir un stream est l'API Fetch. Un objet Response expose son corps sous la forme d'un ReadableStream via response.body, ce qui vous permet de consommer le téléchargement morceau par morceau plutôt que d'attendre la totalité avec response.text().

Pour lire manuellement, appelez getReader() pour verrouiller un lecteur sur le stream, puis bouclez sur reader.read(). Chaque appel se résout en un objet contenant done et value :

const response = await fetch('/large-file.txt');
const reader = response.body.getReader();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  // value is a Uint8Array chunk of bytes
  console.log('Received', value.length, 'bytes');
}

Chaque value est un Uint8Array — un morceau d'octets bruts, pas une string (voir Typed arrays). Lorsque done vaut true, le stream est terminé et value est undefined. Pour convertir les octets en texte, on utilise généralement un TextDecoder, capable d'assembler les morceaux même lorsqu'un caractère multi-octet est réparti sur deux lectures :

javascript— editable

Cette même boucle est la base pour construire des indicateurs de progression de téléchargement : additionnez la longueur de chaque morceau et comparez-la à l'en-tête Content-Length.

Itération asynchrone

Dans les environnements modernes, un ReadableStream est itérable de manière asynchrone, ce qui vous permet de remplacer la boucle manuelle par for await...of (voir itérateurs et générateurs asynchrones) :

const response = await fetch('/large-file.txt');

for await (const chunk of response.body) {
  // chunk is a Uint8Array
  console.log('Received', chunk.length, 'bytes');
}

C'est plus propre car la boucle gère done à votre place et libère automatiquement le lecteur. Le problème est la compatibilité : Node.js le gère bien, mais l'itération asynchrone directe sur response.body reste inégale selon les navigateurs.

Avertissement

Comme la prise en charge de l'itération asynchrone sur les streams est inconsistante dans les navigateurs, la boucle getReader() reste la forme la plus portable. Utilisez for await...of dans Node ou lorsque vous contrôlez l'environnement d'exécution ; repliez-vous sur un lecteur dans le code devant fonctionner partout.

Créer un ReadableStream

Vous pouvez créer votre propre source en passant un objet de source sous-jacente au constructeur ReadableStream. Il peut définir trois méthodes optionnelles :

  • start(controller) s'exécute une fois à la création du stream — idéal pour l'initialisation ou pour pousser des données initiales.
  • pull(controller) est appelée lorsque le consommateur veut plus de données et que la file interne a de la place.
  • cancel(reason) s'exécute si le consommateur arrête de lire prématurément, afin que vous puissiez nettoyer les ressources.

Vous poussez des données avec controller.enqueue(chunk) et signalez la fin avec controller.close() :

javascript— editable

Un stream peut transporter n'importe quelle valeur JavaScript, pas seulement des octets — ici il émet de simples nombres. Lorsque la source est lente ou ouverte (un WebSocket, un minuteur, des données de capteur), placez la logique dans pull() afin que les morceaux soient produits uniquement à la demande du consommateur.

TransformStream

Un TransformStream modifie les morceaux au passage. Vous lui fournissez une fonction transform(chunk, controller) qui reçoit chaque morceau entrant et appelle controller.enqueue() avec le résultat transformé :

const upperCaser = new TransformStream({
  transform(chunk, controller) {
    controller.enqueue(chunk.toUpperCase());
  }
});

Un transform stream expose une extrémité writable (où les morceaux entrent) et une extrémité readable (d'où ils ressortent), ce qui rend justement le piping possible.

La plateforme fournit plusieurs transforms prêts à l'emploi, vous évitant d'écrire manuellement de la logique au niveau des octets :

  • TextDecoderStream / TextEncoderStream convertissent entre des morceaux d'octets et des morceaux de texte.
  • CompressionStream / DecompressionStream appliquent gzip ou deflate à la volée.

Connecter des Streams entre eux

Plutôt que de câbler manuellement des lecteurs et des écrivains, vous pouvez connecter des streams directement. Deux méthodes existent :

  • readable.pipeTo(writable) envoie chaque morceau d'un stream lisible vers un stream accessible en écriture et résout une promesse à la fin.
  • readable.pipeThrough(transformStream) fait transiter les données par un transform et retourne un nouveau stream lisible — parfait pour le chaînage.

Combiner pipeThrough avec TextDecoderStream vous donne directement des morceaux de texte à partir d'une réponse réseau, sans gestion manuelle du décodeur :

const response = await fetch('/large-file.txt');
const textStream = response.body.pipeThrough(new TextDecoderStream());

for await (const textChunk of textStream) {
  console.log(textChunk); // already a string
}

Vous pouvez enchaîner autant d'étapes que vous le souhaitez — par exemple response.body.pipeThrough(new DecompressionStream('gzip')).pipeThrough(new TextDecoderStream()) pour décompresser et décoder dans un seul pipeline déclaratif.

Contre-pression

Un avantage majeur des streams par rapport à la mise en tampon complète est la contre-pression (backpressure). Lorsque le consommateur est lent, le stream signale automatiquement à la source de suspendre la production, et reprend dès que la file se vide. Avec pipeTo et pipeThrough, cela se fait automatiquement — un téléchargement rapide ne devancera pas une écriture disque lente et ne fera pas exploser la mémoire.

Info

La contre-pression est la raison pour laquelle le streaming d'un fichier de plusieurs gigaoctets n'utilise qu'une petite quantité de mémoire bornée. Le producteur ne dépasse jamais de quelques morceaux le consommateur, quelle que soit la taille totale de la charge utile.

Cas d'utilisation

Les streams excellent lorsque les données sont volumineuses, lentes ou continues :

  • Rendu progressif — afficher le début d'une grande réponse pendant que le reste arrive encore, plutôt que de regarder un écran vide.
  • Téléchargements et envois avec progression — mesurer les octets au fil de leur transit pour alimenter une barre de progression.
  • Traitement de gros fichiers — traiter un fichier morceau par morceau pour maintenir une empreinte mémoire constante, même pour des fichiers plus grands que la RAM.
  • Pipelines de compression — passer par CompressionStream ou DecompressionStream pour compresser les données en streaming.

Compatibilité navigateurs et environnements

ReadableStream, WritableStream et TransformStream sont pris en charge dans tous les navigateurs modernes et dans Node.js (où ils sont aussi exposés via node:stream/web). Les points de vigilance concernent les ajouts plus récents : l'itération asynchrone sur response.body et CompressionStream sont arrivés plus tard, vérifiez donc leur support ou prévoyez un repli getReader() lorsque vous avez besoin d'une large couverture. Les streams sont étroitement liés aux Blobsblob.stream() retourne un ReadableStream, permettant d'intégrer des objets de type fichier dans un pipeline de streaming.

Testez vos connaissances

Pratique
Quelle propriété d'une Response fetch est un ReadableStream ?
Quelle propriété d'une Response fetch est un ReadableStream ?
Pratique
Que retourne un appel reader.read() quand le stream est terminé ?
Que retourne un appel reader.read() quand le stream est terminé ?
Pratique
Quelle méthode fait transiter un stream lisible par un transform et retourne un nouveau stream lisible ?
Quelle méthode fait transiter un stream lisible par un transform et retourne un nouveau stream lisible ?
Was this page helpful?