2.3Streams y Buffers: tratar grandes volúmenes
Lo esencial en 30 segundos
- Por trozos, no de golpe: el stream trata cada chunk sobre la marcha — la memoria queda constante sea cual sea el tamaño.
- .pipe() conecta: la lectura fluye hacia la escritura automáticamente, con gestión del caudal — el 80/20 de los streams en una línea.
- Grandes volúmenes = streams: archivos grandes, subidas, flujos de red — archivos pequeños = readFile basta.
Minicaso hilado — Salim copia los archivos sin tumbar el servidor
Salim debe archivar los informes del año: una exportación única de 2 Go. Su primer intento con readFile hace estallar la memoria del pequeño servidor del equipo. Cambia a streams: lectura por trozos, copia hacia el archivo vía .pipe(), luego conducto lectura → compresión → escritura para dividir el tamaño. La memoria casi no se mueve, y el tratamiento empieza desde el primer trozo. En 2.4, será en stream como su servidor enviará los archivos grandes a los clientes.
Continuación del hilo 2.2: los informes de Salim se convierten en un gran volumen que tratar — y continúan en 2.4.
1. El problema de los grandes volúmenes
Imagine tratar un archivo de 2 Go. El enfoque ingenuo lo carga entero en memoria (fs.readFile mete todo el contenido en una variable) — pero 2 Go en memoria es enorme, incluso imposible: la RAM se satura. Los streams (flujos) resuelven este problema: se tratan los datos por trozos (chunks), a medida, como un flujo que corre. Se lee un trozo, se trata, se pasa al siguiente — la memoria solo contiene un pequeño trozo. Eficaz en memoria (volúmenes mayores que la RAM) y en tiempo (se empieza desde el primer trozo, sin esperar la carga completa). Salim lo midió: su exportación de 2 Go pasa con unos pocos Mo de RAM.
2. Los tipos de streams y .pipe()
Cuatro tipos: Readable (lectura desde una fuente: archivo, petición de red), Writable (escritura hacia un destino), Duplex (ambos, como un socket), Transform (transforma al pasar: compresión, cifrado). Los streams son EventEmitters (sección 2-2): se leen vía 'data' (trozo recibido), 'end' (fin), 'error' (problema). Pero la herramienta más elegante es .pipe(): CONECTA una lectura a una escritura, los datos fluyen automáticamente trozo a trozo con gestión del caudal. Copiar un archivo grande: fs.createReadStream('source').pipe(fs.createWriteStream('destination')) — sea cual sea el tamaño. Hasta se encadenan: lecture.pipe(compression).pipe(ecriture).
3. Buffers y cuándo usar los streams
Concepto ligado: los Buffers — Node manipula los datos binarios (archivos, red) vía zonas de memoria bruta de bytes, pues JavaScript no tiene nativamente tipo binario; los streams transportan a menudo Buffers (los trozos). Cuándo usar los streams: ante GRANDES volúmenes — archivos grandes, subidas/descargas (una gran subida en memoria saturaría el servidor: fallo de rendimiento y de seguridad), flujos de red continuos, tratamiento masivo, conductos de transformación, envío de archivos grandes por un servidor. Para archivos pequeños, el readFile clásico basta: simple y legible. Los streams exigen un aprendizaje, pero son una marca de código Node profesional.
🔎 Ejemplo concreto — 🔎 Ejemplo concreto — El archivo comprimido de Salim
El conducto 80/20: leer, comprimir, escribir — sin cargarlo todo:
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'));
Lectura: 2 Go tratados con unos pocos Mo de RAM, tratamiento inmediato.
Lección: encadenar los streams = conducto de transformación.
Vocabulario de la sección
- Stream (flujo)
- Tratamiento de los datos por trozos (chunks), a medida, en lugar de cargarlo todo en memoria; eficaz para los grandes volúmenes.
Ejemplo sencillo: Salim archiva su exportación de 2 Go trozo a trozo.
Lo que no es: un readFile que carga el archivo entero en una variable.
Reflejo: gran volumen = pensar flujo, no carga total.
- Tipos de streams
- Readable (lectura), Writable (escritura), Duplex (ambos, ej. socket), Transform (transforma al pasar: compresión, cifrado).
Ejemplo sencillo: El gzip de Salim es un Transform entre lectura y escritura.
Lo que no es: cuatro sintaxis intercambiables sin distinción.
Reflejo: transformación al pasar = Transform.
- .pipe()
- Conecta un stream de lectura a un stream de escritura: los datos fluyen automáticamente, trozo a trozo, con gestión del caudal.
Ejemplo sencillo: createReadStream('source').pipe(createWriteStream('archive')).
Lo que no es: un bucle manual que acumula todo antes de escribir.
Reflejo: fuente hacia destino = .pipe() primero.
- Buffer
- Zona de memoria bruta de bytes para los datos binarios (archivos, red), que JavaScript no tiene nativamente; los streams los transportan.
Ejemplo sencillo: Cada trozo de la exportación de Salim llega como un Buffer.
Lo que no es: una cadena de caracteres ordinaria.
Reflejo: datos binarios = pensar Buffer.
- Cuándo usar los streams
- En cuanto se tratan grandes volúmenes (archivos grandes, subidas/descargas, flujos de red); para archivos pequeños, readFile basta.
Ejemplo sencillo: Salim transmite la exportación de 2 Go pero lee el inventario (unos Ko) con readFile.
Lo que no es: transmitir sistemáticamente, incluso un archivo de configuración.
Reflejo: tamaño y continuidad del flujo = criterio de elección.
En la práctica — En la práctica — Tratar grandes volúmenes con los streams
- Copie un archivo con un stream: fs.createReadStream('source').pipe(fs.createWriteStream('dest')) — sin cargarlo todo en memoria.
- Lea un stream «a mano» con los eventos: .on('data', chunk => ...), .on('end', ...), .on('error', ...).
- Encadene varios streams con pipe (ej. lectura → compresión gzip → escritura) para un conducto de tratamiento.
- Compare: mida la memoria usada para tratar un archivo grande con readFile (todo en memoria) vs con un stream (por trozos).
Script listo para decir — justificar el stream ante su equipo (30 s)
«Mi primer intento cargaba la exportación entera en memoria y saturó el servidor. En stream, se trata trozo a trozo: la memoria casi no se mueve sea cual sea el tamaño, y se empieza desde el primer trozo. Una línea de pipe basta para copiar, y se encadena la compresión sin almacenar nada entre las etapas.»
Para decir con la medida de memoria a la vista (readFile vs stream); la cifra convence mejor que el principio.
Modelo copiar-pegar — copia robusta y conducto 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);
Archivo pequeño
Inventario de unos Ko: readFile basta — el stream sería complejidad gratuita.
Gran exportación de equipo
Salim comprime la exportación anual en conducto: lectura → gzip → escritura, memoria constante.
Subida de usuario
Recibir un archivo enviado por un cliente en stream: si no una gran subida satura la memoria del servidor.
Lectura a mano
Necesidad de reaccionar a cada trozo (contar líneas): .on('data'/'end'/'error') sobre el Readable.
✓ Buenas prácticas
- Empiece por .pipe() (o pipeline): el 80/20 de los streams en una línea.
- Escuche siempre error en un stream leído a mano.
- Reserve los streams a los grandes volúmenes; guarde readFile para los archivos pequeños.
✗ Errores frecuentes
- Cargar un archivo grande con readFile (RAM saturada, caída).
- Olvidar la escucha error en un stream manual.
- Transmitir sistemáticamente, incluso los archivos minúsculos.
Puntos clave
- Los STREAMS tratan los datos por TROZOS (chunks), a medida, en lugar de cargarlo todo en memoria — eficaz para los GRANDES volúmenes (memoria Y tiempo).
- Tipos: Readable (lectura), Writable (escritura), Duplex, Transform (transforma al pasar: compresión, cifrado). Los streams son EventEmitters (.on('data'/'end'/'error')).
- .pipe() CONECTA un stream de lectura a uno de escritura (los datos fluyen automáticamente, trozo a trozo): readStream.pipe(writeStream). Se encadenan (lectura→compresión→escritura).
- Usar los streams en cuanto se tratan GRANDES volúmenes (archivos grandes, subidas, flujos de red); para archivos pequeños, readFile basta. Marca de código Node de rendimiento.
Desafío 48h — aplique mañana
Tome un archivo grande real (exportación, registro, vídeo) y cópielo con .pipe(), luego comprímalo en conducto lectura → gzip → escritura. Mida la memoria en ambos casos y compare con un readFile.
- Puedo explicar el principio por trozos y la memoria constante.
- Sé copiar y comprimir con pipe/pipeline.
- He medido la diferencia de memoria stream vs readFile sobre un archivo real.
Preguntas frecuentes
¿Cuándo hay que usar realmente los streams en lugar del readFile/writeFile clásico?
En cuanto el tamaño o la continuidad lo exija: readFile carga el archivo entero en memoria — perfecto para unos Ko o Mo, pero catastrófico para cientos de Mo o Go (RAM saturada, incluso caída). Use los streams para los archivos grandes (registros, exportaciones, medios), las subidas/descargas (si no una gran subida satura el servidor), los flujos continuos (respuesta voluminosa, datos en vivo), el tratamiento masivo (enorme CSV línea por línea) y los conductos (leer → comprimir → escribir sin almacenamiento intermedio). Regla de Salim: pequeño y puntual = readFile; grande o continuo = stream.
Los streams parecen complicados: ¿por dónde empezar para aprenderlos progresivamente?
En 4 etapas, de lo simple a lo completo. 1) El principio: tratar por trozos que fluyen (la tubería, no el cubo) — es lo esencial. 2) .pipe() para los casos simples: createReadStream('source').pipe(createWriteStream('dest')) — el 80/20, con gestión del caudal incluida. 3) Encadenar: lecture.pipe(gzip).pipe(ecriture), luego pipeline() del módulo stream para conductos robustos con buena gestión de errores. 4) Leer a mano: .on('data'/'end'/'error') cuando hay que reaccionar a cada trozo. Como Salim: pipe primero, el resto cuando llegue la necesidad.
Salim debe copiar una exportación de 2 Go sin saturar la RAM del servidor. ¿Qué hace?