2.3 · Streams y Buffers: tratar grandes volúmenes

Nivel 2 · Núcleo de Node.js

2.3Streams y Buffers: tratar grandes volúmenes

Objetivo: Tratar grandes volúmenes con eficacia con los streams (por trozos, sin saturar la memoria) y dominar .pipe() — una marca de código Node pro.
Tiempo estimado: 11 min · lectura autónoma · Si solo tienes 5 min: lee «Lo esencial en 30s» + el esquema.

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.

Error frecuente de interpretación: «Los streams son complicados — voy a cargarlo todo en memoria, la RAM aguantará». No. Cargar un archivo grande entero consume su tamaño completo en RAM, bloquea el tratamiento hasta la carga total, y cae más allá de la memoria disponible — en un servidor compartido, es una avería para todos, como descubrió Salim. El 80/20 de los streams cabe en una línea de .pipe(): no hace falta dominar el backpressure ni los Transform a medida para copiar, servir o comprimir archivos grandes. La supuesta complejidad no es excusa: el principio (por trozos, sobre la marcha) basta para empezar.
El flujo en lugar del cubo Memoria constante Fuente Archivo grande,subida, redTrozos Chunks tratados unopor uno (Buffers).pipe() La lectura fluyehacia la escrituraDestino Archivo, respuesta,archivo comprimido
Memoria constante
Versión en texto del esquema (SEO / lectores de pantalla / traducción): 1) La FUENTE de Salim (exportación de 2 Go) no cabe en memoria: adiós readFile. 2) Lee por TROZOS: cada chunk (a menudo un Buffer) se trata y luego se olvida — la memoria queda constante. 3) Conecta con .pipe(): la lectura FLUYE hacia la escritura, con gestión automática del caudal. 4) El DESTINO recibe el archivo (eventualmente comprimido vía un Transform). Lección: grandes volúmenes = flujo sobre la marcha; archivos pequeños = readFile basta.

🔎 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

  1. Copie un archivo con un stream: fs.createReadStream('source').pipe(fs.createWriteStream('dest')) — sin cargarlo todo en memoria.
  2. Lea un stream «a mano» con los eventos: .on('data', chunk => ...), .on('end', ...), .on('error', ...).
  3. Encadene varios streams con pipe (ej. lectura → compresión gzip → escritura) para un conducto de tratamiento.
  4. Compare: mida la memoria usada para tratar un archivo grande con readFile (todo en memoria) vs con un stream (por trozos).
Trata grandes volúmenes con eficacia con los streams (por trozos, sin saturar la memoria) y domina .pipe() para conectar lectura y escritura — una marca de código Node pro.

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.

Prudencia: Una subida tratada en memoria expone el servidor a la saturación (un cliente envía un archivo gigante y la RAM se desploma): trate siempre los archivos entrantes en stream y fije límites de tamaño. Esta guía da los gestos de tratamiento, no una auditoría de seguridad — haga validar los límites por su referente.

✓ 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.

Lista de salida de la sección
  • 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.
Autoevaluación 1-5: ¿elijo stream vs readFile según el tamaño?¿protejo mis streams con una escucha error?

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.

Comprueba tu comprensión

Salim debe copiar una exportación de 2 Go sin saturar la RAM del servidor. ¿Qué hace?

Recursos adicionales (opcional, para ir más allá)

Examen del nivel 2Preguntas inéditas y más difíciles para validar tus conocimientos