2.3 · Streams & Buffers : traiter de gros volumes

Niveau 2 · Cœur de Node.js

2.3Streams & Buffers : traiter de gros volumes

Objectif : Traiter de gros volumes efficacement avec les streams (par morceaux, sans saturer la mémoire) et maîtriser .pipe() — un marqueur de code Node pro.
Temps estimé : 11 min · lecture autonome · Si vous n'avez que 5 min : lisez « L'essentiel en 30s » + le schéma.

L'essentiel en 30 secondes

  • Par morceaux, pas d'un coup : le stream traite chaque chunk au fil de l'eau — la mémoire reste constante quelle que soit la taille.
  • .pipe() connecte : la lecture coule vers l'écriture automatiquement, avec gestion du débit — le 80/20 des streams en une ligne.
  • Gros volumes = streams : gros fichiers, uploads, flux réseau — petits fichiers = readFile suffit.

Mini-cas filé — Salim copie les archives sans faire planter le serveur

Salim doit archiver les rapports de l'année : un export unique de 2 Go. Son premier essai avec readFile fait exploser la mémoire du petit serveur de l'équipe. Il bascule en streams : lecture par morceaux, copie vers l'archive via .pipe(), puis pipeline lecture → compression → écriture pour diviser la taille. La mémoire ne bouge presque pas, et le traitement démarre dès le premier morceau. En 2.4, c'est en stream que son serveur enverra les gros fichiers aux clients.

Suite du fil rouge 2.2 : les rapports de Salim deviennent un gros volume à traiter — et continuent en 2.4.

1. Le problème des gros volumes

Imaginez traiter un fichier de 2 Go. L'approche naïve le charge entièrement en mémoire (fs.readFile met tout le contenu dans une variable) — mais 2 Go en mémoire, c'est énorme, voire impossible : la RAM sature. Les streams (flux) résolvent ce problème : on traite les données par morceaux (chunks), au fur et à mesure, comme un flux qui s'écoule. On lit un morceau, on le traite, on passe au suivant — la mémoire ne contient jamais qu'un petit morceau. Efficace en mémoire (volumes plus gros que la RAM) et en temps (on commence dès le premier morceau, sans attendre le chargement complet). Salim l'a mesuré : son export de 2 Go passe avec quelques Mo de RAM.

2. Les types de streams et .pipe()

Quatre types : Readable (lecture depuis une source : fichier, requête réseau), Writable (écriture vers une destination), Duplex (les deux, comme une socket), Transform (transforme au passage : compression, chiffrement). Les streams sont des EventEmitters (section 2-2) : on les lit via 'data' (morceau reçu), 'end' (fin), 'error' (problème). Mais l'outil le plus élégant est .pipe() : il CONNECTE une lecture à une écriture, les données coulent automatiquement morceau par morceau avec gestion du débit. Copier un gros fichier : fs.createReadStream('source').pipe(fs.createWriteStream('destination')) — quelle que soit la taille. On enchaîne même : lecture.pipe(compression).pipe(ecriture).

3. Buffers et quand utiliser les streams

Concept lié : les Buffers — Node manipule les données binaires (fichiers, réseau) via des zones de mémoire brute d'octets, car JavaScript n'a pas nativement de type binaire ; les streams transportent souvent des Buffers (les morceaux). Quand utiliser les streams : dès les GROS volumes — gros fichiers, uploads/téléchargements (un gros upload en mémoire saturerait le serveur : faille de performance et de sécurité), flux réseau continus, traitement de masse, pipelines de transformation, envoi de gros fichiers par un serveur. Pour de petits fichiers, readFile classique suffit : simple et lisible. Les streams demandent un apprentissage, mais ce sont un marqueur de code Node professionnel.

Erreur d'interprétation fréquente : « Les streams, c'est compliqué — je vais tout charger en mémoire, la RAM suivra ». Non. Charger un gros fichier en entier consomme sa taille complète en RAM, bloque le traitement jusqu'au chargement total, et plante au-delà de la mémoire disponible — sur un serveur partagé, c'est une panne pour tout le monde, comme Salim l'a découvert. Le 80/20 des streams tient en une ligne de .pipe() : pas besoin de maîtriser le backpressure ou les Transform sur mesure pour copier, servir ou compresser de gros fichiers. La complexité supposée n'est pas une excuse : le principe (par morceaux, au fil de l'eau) suffit pour commencer.
Le flux au lieu du seau Mémoire constante Source Gros fichier,upload, réseauMorceaux Chunks traités unpar un (Buffers).pipe() La lecture coulevers l'écritureDestination Archive,réponse, fichier compressé
Mémoire constante
Version texte du schéma (SEO / lecteurs d'écran / traduction) : 1) La SOURCE de Salim (export de 2 Go) ne tient pas en mémoire : exit readFile. 2) Il lit par MORCEAUX : chaque chunk (souvent un Buffer) est traité puis oublié — la mémoire reste constante. 3) Il connecte avec .pipe() : la lecture COULE vers l'écriture, avec gestion du débit automatique. 4) La DESTINATION reçoit l'archive (éventuellement compressée via un Transform). Leçon : gros volumes = flux au fil de l'eau ; petits fichiers = readFile suffit.

🔎 Exemple concret — 🔎 Exemple concret — L'archive compressée de Salim

Le pipeline 80/20 : lire, compresser, écrire — sans tout charger :

const fs = require('node:fs');
const zlib = require('node:zlib');

fs.createReadStream('export-2026.csv')
  .pipe(zlib.createGzip())
  .pipe(fs.createWriteStream('export-2026.csv.gz'))
  .on('finish', () => console.log('Archive prête'));

Lecture : 2 Go traités avec quelques Mo de RAM, traitement immédiat.

Leçon : enchaîner les streams = pipeline de transformation.

Vocabulaire de la section

Stream (flux)
Traitement des données par morceaux (chunks), au fur et à mesure, plutôt que tout charger en mémoire ; efficace pour les gros volumes.

Exemple simple : Salim archive son export de 2 Go morceau par morceau.

Ce que ce n'est pas : un readFile qui charge le fichier entier dans une variable.

Réflexe : gros volume = penser flux, pas chargement total.

Types de streams
Readable (lecture), Writable (écriture), Duplex (les deux, ex. socket), Transform (transforme au passage : compression, chiffrement).

Exemple simple : Le gzip de Salim est un Transform entre lecture et écriture.

Ce que ce n'est pas : quatre syntaxes interchangeables sans distinction.

Réflexe : transformation au passage = Transform.

.pipe()
Connecte un stream de lecture à un stream d'écriture : les données coulent automatiquement, morceau par morceau, avec gestion du débit.

Exemple simple : createReadStream('source').pipe(createWriteStream('archive')).

Ce que ce n'est pas : une boucle manuelle qui accumule tout avant d'écrire.

Réflexe : source vers destination = .pipe() d'abord.

Buffer
Zone de mémoire brute d'octets pour les données binaires (fichiers, réseau), que JavaScript n'a pas nativement ; les streams en transportent.

Exemple simple : Chaque morceau de l'export de Salim arrive comme un Buffer.

Ce que ce n'est pas : une chaîne de caractères ordinaire.

Réflexe : données binaires = penser Buffer.

Quand utiliser les streams
Dès qu'on traite de gros volumes (gros fichiers, uploads/téléchargements, flux réseau) ; pour de petits fichiers, readFile suffit.

Exemple simple : Salim streame l'export de 2 Go mais lit l'inventaire (quelques Ko) avec readFile.

Ce que ce n'est pas : streamer systématiquement, même un fichier de configuration.

Réflexe : taille et continuité du flux = critère de choix.

En pratique — En pratique — Traiter de gros volumes avec les streams

  1. Copiez un fichier avec un stream : fs.createReadStream('source').pipe(fs.createWriteStream('dest')) — sans tout charger en mémoire.
  2. Lisez un stream « à la main » avec les événements : .on('data', chunk => ...), .on('end', ...), .on('error', ...).
  3. Enchaînez plusieurs streams avec pipe (ex. lecture → compression gzip → écriture) pour un pipeline de traitement.
  4. Comparez : mesurez la mémoire utilisée pour traiter un gros fichier avec readFile (tout en mémoire) vs avec un stream (par morceaux).
Vous traitez de gros volumes efficacement avec les streams (par morceaux, sans saturer la mémoire) et maîtrisez .pipe() pour connecter lecture et écriture — un marqueur de code Node pro.

Script prêt à dire — justifier le stream à son équipe (30 s)

« Mon premier essai chargeait l'export entier en mémoire et a saturé le serveur. En stream, on traite morceau par morceau : la mémoire ne bouge presque pas quelle que soit la taille, et on commence dès le premier morceau. Une ligne de pipe suffit pour copier, et on enchaîne la compression sans rien stocker entre les étapes. »

À dire avec la mesure mémoire sous les yeux (readFile vs stream) ; le chiffre convainc mieux que le principe.

Modèle copier-coller — copie robuste et pipeline gzip

const fs = require('node:fs');
const zlib = require('node:zlib');
const { pipeline } = require('node:stream/promises');

async function copier(source, destination) {
  await pipeline(
    fs.createReadStream(source),
    fs.createWriteStream(destination)
  );
  console.log('Copié : ' + destination);
}

async function compresser(source, destination) {
  await pipeline(
    fs.createReadStream(source),
    zlib.createGzip(),
    fs.createWriteStream(destination)
  );
  console.log('Compressé : ' + destination);
}

copier('export-2026.csv', 'archive-2026.csv').catch(console.error);

Petit fichier

Inventaire de quelques Ko : readFile suffit — le stream serait de la complexité gratuite.

Gros export d'équipe

Salim compresse l'export annuel en pipeline : lecture → gzip → écriture, mémoire constante.

Upload utilisateur

Recevoir un fichier envoyé par un client en stream : sinon un gros upload sature la mémoire du serveur.

Lecture à la main

Besoin de réagir à chaque morceau (compter des lignes) : .on('data'/'end'/'error') sur le Readable.

Prudence : Un upload traité en mémoire expose le serveur à la saturation (un client envoie un fichier géant et la RAM s'écroule) : traitez toujours les fichiers entrants en stream et fixez des limites de taille. Ce guide donne les gestes de traitement, pas un audit de sécurité — faites valider les limites par votre référent.

✓ Bonnes pratiques

  • Commencez par .pipe() (ou pipeline) : le 80/20 des streams en une ligne.
  • Écoutez toujours error sur un stream lu à la main.
  • Réservez les streams aux gros volumes ; gardez readFile pour les petits fichiers.

✗ Pièges fréquents

  • Charger un gros fichier avec readFile (RAM saturée, plantage).
  • Oublier l'écouteur error sur un stream manuel.
  • Streamer systématiquement, même les fichiers minuscules.

Points clés à retenir

  • Les STREAMS traitent les données par MORCEAUX (chunks), au fur et à mesure, au lieu de tout charger en mémoire — efficace pour les GROS volumes (mémoire ET temps).
  • Types : Readable (lecture), Writable (écriture), Duplex, Transform (transforme au passage : compression, chiffrement). Les streams sont des EventEmitters (.on('data'/'end'/'error')).
  • .pipe() CONNECTE un stream de lecture à un d'écriture (les données coulent automatiquement, morceau par morceau) : readStream.pipe(writeStream). On enchaîne (lecture→compression→écriture).
  • Utiliser les streams dès qu'on traite de GROS volumes (gros fichiers, uploads, flux réseau) ; pour de petits fichiers, readFile suffit. Marqueur de code Node performant.

Défi 48h — appliquez demain

Prenez un vrai gros fichier (export, log, vidéo) et copiez-le avec .pipe(), puis compressez-le en pipeline lecture → gzip → écriture. Mesurez la mémoire dans les deux cas et comparez à un readFile.

Checklist sortie de section
  • Je peux expliquer le principe par morceaux et la mémoire constante.
  • Je sais copier et compresser avec pipe/pipeline.
  • J'ai mesuré l'écart mémoire stream vs readFile sur un fichier réel.
Auto-évaluation 1-5 : est-ce que je choisis stream vs readFile selon la taille ?est-ce que je protège mes streams avec un écouteur error ?

Questions fréquentes

Quand faut-il vraiment utiliser les streams plutôt que readFile/writeFile classique ?

Dès que la taille ou la continuité l'exige : readFile charge le fichier entier en mémoire — parfait pour quelques Ko ou Mo, mais catastrophique pour des centaines de Mo ou des Go (RAM saturée, voire plantage). Utilisez les streams pour les gros fichiers (logs, exports, médias), les uploads/téléchargements (sinon un gros upload sature le serveur), les flux continus (réponse volumineuse, données live), le traitement de masse (énorme CSV ligne par ligne) et les pipelines (lire → compresser → écrire sans stockage intermédiaire). Règle de Salim : petit et ponctuel = readFile ; gros ou continu = stream.

Les streams semblent compliqués : par où commencer pour les apprendre progressivement ?

En 4 étapes, du simple au complet. 1) Le principe : traiter par morceaux qui s'écoulent (le tuyau, pas le seau) — c'est l'essentiel. 2) .pipe() pour les cas simples : createReadStream('source').pipe(createWriteStream('dest')) — le 80/20, avec gestion du débit incluse. 3) Enchaîner : lecture.pipe(gzip).pipe(ecriture), puis pipeline() du module stream pour des pipelines robustes avec bonne gestion d'erreurs. 4) Lire à la main : .on('data'/'end'/'error') quand il faut réagir à chaque morceau. Comme Salim : pipe d'abord, le reste quand le besoin arrive.

Vérifiez votre compréhension

Salim doit copier un export de 2 Go sans saturer la RAM du serveur. Que fait-il ?

Ressources supplémentaires (optionnel, pour aller plus loin)

Examen du niveau 2Questions inédites et durcies pour valider vos acquis