Node.js I/O

Node.js Streams

Les streams traitent les données par morceaux (chunks) au lieu de tout charger en mémoire. C'est ainsi que Node copie des fichiers, compresse des payloads et achemine les corps HTTP efficacement.

intermediate15 min readUpdated 15 sept. 2026
compress.js
js
// compress.js
import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { createGzip } from "node:zlib";

await pipeline(
  createReadStream("access.log"),
  createGzip(),
  createWriteStream("access.log.gz"),
);
Taille du chunk
highWaterMark
Types
Readable, Writable, Duplex, Transform
Connexion
pipe et pipeline
Contrôle de flux
Backpressure
Itération
for await
Idéal pour
Données volumineuses ou illimitées

Pourquoi c'est important

Pourquoi les streams sont essentiels

Mémoire constante

Un stream ne conserve qu'un seul chunk à la fois, vous permettant ainsi de traiter des gigaoctets avec seulement quelques mégaoctets de mémoire.

Backpressure naturelle

Un consommateur lent signale au producteur de ralentir, évitant ainsi que des sources rapides ne saturent la mémoire ou un disque lent.

Pipelines composables

Les étapes Readable, Transform et Writable s'assemblent parfaitement, transformant des traitements complexes en chaînes lisibles.

Le tableau complet

Les trois concepts fondamentaux des streams

Les données circulent par chunks, la backpressure équilibre le producteur et le consommateur, et le pipeline relie les différentes étapes.

Chunks

Flux

Les données se déplacent sous forme de buffers ou d'objets plutôt que comme une seule valeur massive.

Backpressure

Équilibre

Les buffers internes et le highWaterMark empêchent les producteurs de dépasser la capacité des consommateurs.

Pipeline

Composition

pipeline connecte les étapes et propage les erreurs ainsi que le nettoyage.

Les streams en un coup d'œil

Le cœur des streams

Readable

Une source depuis laquelle on lit, comme un fichier, un socket ou le corps d'une requête.

Writable

Une destination dans laquelle on écrit, comme un fichier ou une réponse.

Duplex

À la fois readable et writable, comme un socket TCP.

Transform

Une étape duplex qui modifie les données, comme gzip ou un parseur.

Backpressure

write retourne false et vous attendez l'événement drain.

Object mode

Stream d'objets au lieu d'octets pour des pipelines structurés.

Un bref aperçu

L'évolution des streams

  1. 2010

    Arrivée des streams

    Node introduit les streams pour gérer les données de manière incrémentale.

    10
  2. 2012

    Streams 2

    Une API repensée ajoute pipe et la backpressure.

    12
  3. 2017

    Streams 3

    Une sémantique plus claire et pipeline améliorent la gestion des erreurs.

    17
  4. 2018

    Itération asynchrone

    for await of permet de lire les streams comme s'il s'agissait de tableaux.

    18
  5. Aujourd'hui

    Omniprésents

    HTTP, fichiers, compression, crypto et de nombreuses bibliothèques reposent sur les streams.

    Aujourd'hui

Le guide complet

Node.js Streams: Tout ce que vous devez savoir

Pourquoi les streams sont-ils importants ?

Charger un fichier volumineux en mémoire fonctionne jusqu’à ce que le fichier devienne plus grand que la mémoire disponible. Les streams résolvent ce problème en traitant les données par morceaux (chunks) : vous lisez une partie, vous la traitez, puis vous passez à la suivante, ce qui permet à la consommation mémoire de rester approximativement constante, quelle que soit la taille de l’entrée.

Les streams sont omniprésents dans Node.js. Les corps des requêtes et réponses HTTP, la lecture et l’écriture de fichiers, la compression, le chiffrement et de nombreux analyseurs (parsers) sont tous des streams. Les comprendre est essentiel pour bâtir des serveurs et des outils capables de gérer des données illimitées sans planter.

Les quatre types de streams

Chaque stream appartient à l’une des quatre catégories suivantes :

  • Readable — une source depuis laquelle on lit des données. Fichiers, corps de requêtes HTTP, sockets et process.stdin.
  • Writable — une destination vers laquelle on écrit des données. Fichiers, réponses HTTP, sockets et process.stdout.
  • Duplex — à la fois readable et writable, comme un socket TCP.
  • Transform — un stream duplex qui modifie les données lors de leur passage, comme zlib.createGzip() ou un parseur CSV.

Les streams readable et writable se connectent via des pipelines, et les transforms se placent au milieu.

Lecture et écriture

Le moyen le plus simple de consommer un readable stream est l’itération asynchrone.

// read.js
import { createReadStream } from "node:fs";

const stream = createReadStream("access.log", { encoding: "utf8" });

for await (const chunk of stream) {
  process.stdout.write(chunk);
}

Pour les writable streams, appelez write() et signalez la fin avec end().

// write.js
import { createWriteStream } from "node:fs";

const out = createWriteStream("out.txt");

out.write("first line\n");
out.write("second line\n");
out.end();

Le stream gère la mise en mémoire tampon des écritures en interne et les vide efficacement, vous n’avez donc pas à gérer les chunks vous-même.

Piping et pipeline

Un pipeline connecte un flux lisible (readable), zéro ou plusieurs transformations (transforms), et un flux scriptible (writable).

// compress.js
import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { createGzip } from "node:zlib";

await pipeline(
  createReadStream("access.log"),
  createGzip(),
  createWriteStream("access.log.gz"),
);

Privilégiez toujours pipeline au chaînage manuel de .pipe(). pipeline transmet les erreurs de chaque étape, détruit les flux en cas d’échec et retourne une promesse, ce qui simplifie grandement la gestion des erreurs. Le .pipe() manuel ne propage pas les erreurs, donc un échec en amont peut laisser le pipeline en suspens et la destination ouverte.

Backpressure

Les streams possèdent des buffers internes. Lorsque vous écrivez des données plus rapidement que la destination ne peut les consommer, le buffer se remplit et write() retourne false.

// backpressure.js
function writeAll(stream, chunks) {
  return new Promise((resolve, reject) => {
    let i = 0;
    const next = () => {
      while (i < chunks.length) {
        const ok = stream.write(chunks[i++]);
        if (!ok) {
          stream.once("drain", next);
          return;
        }
      }
      stream.end(resolve);
    };
    stream.on("error", reject);
    next();
  });
}

Attendre drain avant d’écrire davantage est la manière dont un producteur respecte un consommateur lent. pipeline et pipe gèrent cela automatiquement, ce qui est une raison supplémentaire de les utiliser.

Streams de transformation (Transform streams)

Un transform stream applique une fonction à chaque chunk. Vous pouvez créer le vôtre.

// upper.js
import { Transform } from "node:stream";

const upper = new Transform({
  transform(chunk, encoding, callback) {
    callback(null, chunk.toString().toUpperCase());
  },
});

process.stdin.pipe(upper).pipe(process.stdout);

Node propose des transformations utiles dans node:zlib (gzip, deflate), node:crypto (chiffrements et hachages) ainsi que dans de nombreuses bibliothèques, comme les parseurs CSV et JSON.

Mode objet

Par défaut, les streams transportent des octets. Le mode objet leur permet de transporter des objets JavaScript à la place, ce qui est idéal pour les pipelines structurés.

// object-mode.js
import { Readable, Transform } from "node:stream";

Readable.from([{ id: 1 }, { id: 2 }])
  .pipe(
    new Transform({
      objectMode: true,
      transform(record, _encoding, callback) {
        callback(null, { ...record, seen: true });
      },
    }),
  )
  .on("data", console.log);

En mode objet, highWaterMark compte les objets plutôt que les octets. C’est ainsi que sont conçus les streams de lignes de base de données, les processeurs de logs et les pipelines ETL.

Patterns courants

  • Copier un fichier : pipeline(createReadStream(src), createWriteStream(dest)).
  • Compresser ou chiffrer : insérer createGzip() ou une transformation de chiffrement.
  • Diffuser une réponse HTTP : rediriger (pipe) un fichier ou le résultat d’une requête directement vers res.
  • Analyser des données délimitées par ligne : utiliser une transformation qui fragmente le flux aux sauts de ligne.
  • Progression d’un upload : compter les octets dans une transformation au fur et à mesure de leur passage.

Bonnes pratiques

  • Privilégiez pipeline à .pipe() pour la gestion des erreurs et le nettoyage.
  • Utilisez des flux (streams) pour les fichiers et les réponses volumineux au lieu de les mettre en mémoire tampon (buffering).
  • Respectez la contre-pression (backpressure) ; n’ignorez jamais le fait que write() retourne false.
  • N’ajustez highWaterMark qu’après avoir effectué des mesures.
  • Utilisez le mode objet pour les données structurées.
  • Gérez error sur chaque flux que vous créez.
  • Détruisez les flux en cas d’échec afin de libérer les descripteurs de fichiers.

Erreurs courantes

  • Utiliser readFile pour des données volumineuses ou non bornées.
  • Chaîner des .pipe() et perdre ainsi les erreurs.
  • Ignorer la backpressure et mettre en tampon des données non bornées.
  • Oublier qu’un callback de transformation doit être appelé exactement une fois.
  • Mélanger les encodages, ce qui produit une sortie corrompue.
  • Laisser des streams ouverts après une erreur.

Et après ?

Les streams sont le moyen utilisé par Node.js pour gérer les données à grande échelle. Mettez-les en pratique avec le guide sur le système de fichiers et comprenez le fonctionnement de l’ordonnancement dans le guide sur l’Event Loop. Ensuite, réécrivez un appel readFile sous forme de stream et observez votre consommation mémoire se stabiliser.

Connexion des étapes de stream

pipeline gère les erreurs, ferme chaque étape et effectue le nettoyage. pipe manuel ne transfère pas les erreurs, ce qui peut causer des blocages ou des fuites.

Préférer
import { pipeline } from "node:stream/promises";

await pipeline(source, transform, destination);
Éviter
source.pipe(transform).pipe(destination);
// errors on source are not
// forwarded to destination

Lecture d'un fichier volumineux

Un stream maintient une consommation mémoire stable. readFile charge tout le fichier en mémoire, ce qui convient aux petits fichiers mais est dangereux pour les gros.

Préférer
import { createReadStream } from "node:fs";

const stream = createReadStream("big.log");
for await (const chunk of stream) {
  handle(chunk);
}
Éviter
import { readFile } from "node:fs/promises";

// the whole file in memory
const data = await readFile("big.log");
handle(data);

FAQ

Foire aux questions

Keep learning

Related topics from the roadmap.

$ commencer à apprendre

Prêt à apprendre Streams ?

Notre tutoriel interactif vous guide à travers Streams pas à pas — avec des quiz et du vrai code que vous pouvez exécuter dans le navigateur.