2.3Streams & Buffers : traiter de gros volumes
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.
🔎 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
- Copiez un fichier avec un stream : fs.createReadStream('source').pipe(fs.createWriteStream('dest')) — sans tout charger en mémoire.
- Lisez un stream « à la main » avec les événements : .on('data', chunk => ...), .on('end', ...), .on('error', ...).
- Enchaînez plusieurs streams avec pipe (ex. lecture → compression gzip → écriture) pour un pipeline de traitement.
- Comparez : mesurez la mémoire utilisée pour traiter un gros fichier avec readFile (tout en mémoire) vs avec un stream (par morceaux).
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.
✓ 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.
- 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.
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.
Salim doit copier un export de 2 Go sans saturer la RAM du serveur. Que fait-il ?