2.3Streams & Buffers (traiter de gros volumes)
Les streams (flux) sont l'un des concepts les plus puissants — et parfois intimidants — de Node, mais leur idée est simple et cruciale pour la performance. Imaginez que vous devez traiter un fichier de 2 Go. L'approche naïve serait de le charger ENTIÈREMENT en mémoire (fs.readFile met tout le contenu dans une variable), puis de le traiter — mais 2 Go en mémoire, c'est énorme, voire impossible (vous saturez la RAM). Les streams résolvent ce problème : au lieu de tout charger d'un coup, 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 à la fois. C'est efficace en mémoire (on traite des volumes bien plus gros que la RAM) et en temps (on commence à traiter dès le premier morceau, sans attendre le chargement complet).
Il existe plusieurs types de streams : Readable (flux de lecture — on lit des données depuis une source : un fichier, une requête réseau), Writable (flux d'écriture — on écrit des données vers une destination : un fichier, une réponse réseau), Duplex (lecture ET écriture, comme une socket), Transform (transforme les données au passage — compression, chiffrement). Les streams sont des EventEmitters (section 2-2) : on les manipule via des événements ('data' pour chaque morceau reçu, 'end' à la fin, 'error' en cas de problème). Mais l'outil le plus élégant est .pipe() : il CONNECTE un stream de lecture à un stream d'écriture, faisant « couler » les données de l'un vers l'autre automatiquement, morceau par morceau, avec gestion du débit. Exemple emblématique : copier un gros fichier — fs.createReadStream('source').pipe(fs.createWriteStream('destination')) — copie efficacement quelle que soit la taille, sans jamais tout charger en mémoire. On enchaîne même plusieurs streams (lecture.pipe(compression).pipe(ecriture)). Un concept lié : les Buffers — Node manipule les données binaires (fichiers, réseau) via des Buffers (des zones de mémoire brute d'octets), car JavaScript n'a pas nativement de type pour les données binaires. Les streams transportent souvent des Buffers (les morceaux de données). Quand utiliser les streams : dès que vous traitez de GROS volumes (gros fichiers, gros téléchargements/uploads, flux réseau continus, traitement de données en masse) — là où charger tout en mémoire serait inefficace ou impossible. Pour de petits fichiers, readFile classique suffit. Les streams demandent un peu d'apprentissage (le modèle par morceaux, les événements, le pipe) mais sont indispensables pour la performance et la scalabilité sur les gros volumes — un marqueur de code Node professionnel. Comprendre au moins le principe (traiter par morceaux plutôt que tout charger) et savoir utiliser .pipe() pour les cas courants est une compétence précieuse.
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 (mémoire et temps).
- Types de streams
- Readable (lecture), Writable (écriture), Duplex (les deux, ex. socket), Transform (transforme au passage : compression, chiffrement).
- .pipe()
- Connecte un stream de lecture à un stream d'écriture : les données coulent automatiquement de l'un à l'autre, morceau par morceau, avec gestion du débit.
- Buffer
- Zone de mémoire brute d'octets pour manipuler les données binaires (fichiers, réseau), que JavaScript n'a pas nativement ; les streams transportent souvent des Buffers.
- 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 classique suffit.
Quand utiliser les STREAMS plutôt que readFile/writeFile ?
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).
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.
Questions fréquentes
Quand faut-il vraiment utiliser les streams plutôt que readFile/writeFile classique ?
La règle est claire : utilisez les streams pour les GROS volumes ou les flux continus, et readFile/writeFile pour les petites données ponctuelles — la clé est la question de la MÉMOIRE et de la nature des données. Le problème que les streams résolvent : fs.readFile charge le fichier ENTIER en mémoire (dans une variable) avant que vous ne puissiez le traiter. Pour un petit fichier (quelques Ko, quelques Mo), c'est parfait et simple. Mais pour un GROS fichier (centaines de Mo, Go), c'est problématique : (1) vous consommez énormément de RAM (le fichier entier en mémoire) ; (2) au-delà d'une certaine taille, c'est impossible (vous dépassez la mémoire disponible, le programme plante) ; (3) même si ça passe, c'est du gaspillage (pourquoi charger 2 Go pour les traiter séquentiellement ?). Les streams traitent par MORCEAUX : la mémoire ne contient jamais qu'un petit morceau à la fois, quelle que soit la taille totale. Vous pouvez traiter un fichier de 100 Go avec quelques Mo de RAM. Quand utiliser les STREAMS : (1) Gros fichiers : lire, écrire, copier, transformer des fichiers volumineux (logs, données, médias, exports). (2) Uploads et téléchargements : recevoir un fichier uploadé par un utilisateur ou en envoyer un — on le traite en flux (sinon un gros upload saturerait la mémoire du serveur, une faille de performance et de sécurité). (3) Flux réseau continus : traiter des données qui arrivent en continu (une réponse HTTP volumineuse, un flux de données live). (4) Traitement de données en masse : lire un énorme fichier CSV/JSON ligne par ligne, transformer un gros volume au fil de l'eau. (5) Pipelines de transformation : lire → compresser → chiffrer → écrire, en flux, sans stocker les étapes intermédiaires en mémoire. (6) Servir de gros fichiers : un serveur qui envoie un gros fichier à un client le fait en stream (le client reçoit au fur et à mesure, la mémoire serveur reste faible). Quand readFile/writeFile CLASSIQUE suffit : (1) Petits fichiers : config, petits fichiers de données, fichiers texte courts — les charger entièrement est simple et sans conséquence (quelques Ko en mémoire, aucun problème). (2) Quand vous avez besoin du contenu ENTIER d'un coup : par exemple parser un fichier JSON de config (vous devez avoir tout le JSON pour le parser) — un stream n'aiderait pas et compliquerait. (3) Simplicité : readFile/writeFile sont plus simples à écrire ; pour de petites données, cette simplicité est préférable (ne pas sur-ingénierer). Le critère de décision : (1) La taille : gros ou potentiellement gros (surtout si la taille dépend d'entrées utilisateur — un upload dont vous ne contrôlez pas la taille DOIT être en stream) → streams. Petit et borné → readFile. (2) La nature : flux continu (réseau, live) → streams. Données ponctuelles complètes → readFile. (3) La mémoire : si charger tout risque de saturer la RAM → streams. Attention particulière aux uploads : un piège classique est de recevoir un fichier uploadé en le chargeant entièrement en mémoire — un attaquant (ou un gros fichier légitime) peut alors saturer votre serveur. Les uploads se traitent en STREAM (les bibliothèques d'upload le font). C'est un enjeu de performance ET de sécurité (résistance à la saturation). Le bon réflexe : (1) petites données ponctuelles → readFile/writeFile (simple) ; (2) gros volumes, tailles non bornées, flux continus, pipelines → streams (efficace, scalable). En cas de doute sur des données dont la taille peut être grande ou incontrôlée, privilégiez les streams — c'est le choix scalable. Maîtriser les streams (au moins le principe et .pipe()) vous permet d'écrire des applications qui tiennent la charge sur de gros volumes, là où une approche « tout en mémoire » s'effondrerait. C'est un marqueur de code Node professionnel et performant. Commencez par les cas simples (.pipe() pour copier/servir de gros fichiers) et approfondissez selon vos besoins.
Les streams semblent compliqués : par où commencer pour les apprendre progressivement ?
Les streams ont une réputation d'intimidation méritée (le modèle par morceaux, les événements, les types, le backpressure...), mais on peut les apprendre PROGRESSIVEMENT en commençant par les usages simples et concrets, sans tout maîtriser d'un coup. Voici un parcours d'apprentissage graduel. Étape 1 — Comprendre le PRINCIPE (le plus important) : avant toute syntaxe, saisissez l'idée : au lieu de charger tout en mémoire, on traite les données par MORCEAUX qui « s'écoulent ». L'analogie : plutôt que de remplir un seau entier puis de le porter (charger tout), on laisse l'eau couler par un tuyau et on la traite au fil de l'écoulement (stream). Cette intuition (par morceaux, au fil de l'eau, mémoire constante) est l'essentiel — le reste est de la mécanique. Étape 2 — Utiliser .pipe() pour les cas simples : commencez par l'usage le plus courant et le plus simple, le .pipe(). Copier un fichier : fs.createReadStream('source').pipe(fs.createWriteStream('dest')). Vous connectez une source à une destination, et Node fait couler les données automatiquement (y compris la gestion du débit — le « backpressure » — sans que vous ayez à vous en soucier). C'est puissant, concis, et couvre BEAUCOUP de besoins réels (copier, servir, transférer de gros fichiers) sans complexité. Maîtrisez .pipe() d'abord — c'est le 80/20 des streams. Étape 3 — Enchaîner des streams avec pipe : une fois à l'aise avec un pipe simple, enchaînez : lecture.pipe(transformation).pipe(ecriture) — par exemple lire un fichier, le compresser (avec le module zlib), l'écrire. Vous découvrez les streams Transform (qui transforment au passage) et la puissance des pipelines. Node fournit aussi pipeline() (module stream) qui gère mieux les erreurs qu'un chaînage de .pipe() — à connaître pour les pipelines robustes. Étape 4 — Lire un stream avec les événements : quand vous avez besoin de TRAITER les données (pas juste les transférer), apprenez à écouter les événements : .on('data', chunk => ...) (traiter chaque morceau), .on('end', ...) (fin), .on('error', ...) (erreur — toujours la gérer). C'est utile pour, par exemple, traiter un gros fichier ligne par ligne, accumuler ou analyser des données au fil du flux. Étape 5 — Approfondir selon les besoins : les concepts avancés (créer ses propres streams Readable/Writable/Transform, le backpressure en détail, les modes objet, async iterators sur les streams) s'apprennent QUAND un besoin concret se présente — pas d'un coup, pas « au cas où ». Beaucoup de développeurs utilisent surtout .pipe() et l'écoute d'événements, et n'ont que rarement besoin de créer des streams custom. Conseils pour progresser sans se décourager : (1) Commencez par le PRINCIPE et .pipe() — ne vous jetez pas sur la théorie complète. Un pipe simple qui marche vous donne une victoire concrète et la compréhension de base. (2) Apprenez par des cas réels : copier un gros fichier, servir un fichier depuis un serveur, compresser — des besoins concrets qui ancrent la compréhension. (3) Acceptez de ne pas tout comprendre d'emblée : les streams sont un sujet où l'on approfondit par couches, au fil des besoins. Savoir les UTILISER (pipe, écouter les événements) suffit pour l'immense majorité des cas ; les créer/maîtriser en profondeur vient plus tard, si nécessaire. (4) Gérez toujours les erreurs ('error') — un oubli fréquent qui cause des plantages ; prenez le réflexe dès le début. (5) Les bibliothèques abstraient souvent les streams : quand vous utiliserez Express (envoyer un fichier), Multer (uploads), etc., les streams travaillent souvent en coulisses — vous bénéficiez de leur efficacité sans tout gérer à la main. Comprendre le principe vous aide à comprendre ce que ces outils font. En résumé : ne soyez pas intimidé — commencez par le PRINCIPE (traiter par morceaux) et .pipe() (le cas simple et puissant), progressez vers l'écoute d'événements pour traiter des données, et approfondissez au fil des besoins réels. Vous n'avez pas besoin de maîtriser toute la théorie des streams pour en tirer une valeur énorme (traiter efficacement de gros volumes). Les streams récompensent une approche progressive et pratique — un pas après l'autre, en commençant par les usages concrets.