W3docs

JavaScript Streams API

Aprende la Streams API de JavaScript: lee datos progresivamente con ReadableStream, escribe con WritableStream, transforma con TransformStream y conecta streams para procesar datos grandes de forma eficiente.

La Streams API permite procesar datos en pequeños fragmentos a medida que llegan, en lugar de cargar todo en memoria de una vez. Esto es esencial para trabajar con archivos grandes, respuestas de red lentas y datos en tiempo real: puedes empezar a manejar los primeros bytes mientras el resto aún está en tránsito, y nunca tienes que mantener toda la carga útil en memoria.

La API se construye alrededor de tres tipos principales. Un ReadableStream es una fuente de la que extraes datos. Un WritableStream es un destino al que envías datos. Un TransformStream se sitúa en el medio, recibiendo fragmentos por un extremo y emitiendo fragmentos modificados por el otro. Una vez que comprendes estos tres, puedes componerlos en pipelines eficientes.

Leer un Stream

La forma más común de obtener un stream es la Fetch API. Un objeto Response expone su cuerpo como un ReadableStream a través de response.body, por lo que puedes consumir la descarga fragmento a fragmento en lugar de esperar todo con response.text().

Para leer manualmente, llama a getReader() para bloquear un lector al stream y luego itera con reader.read(). Cada llamada resuelve en un objeto con done y value:

const response = await fetch('/large-file.txt');
const reader = response.body.getReader();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  // value is a Uint8Array chunk of bytes
  console.log('Received', value.length, 'bytes');
}

Cada value es un Uint8Array — un fragmento de bytes sin procesar, no una string (véase arrays tipados). Cuando done es true, el stream ha finalizado y value es undefined. Para convertir los bytes en texto normalmente se usa un TextDecoder, que puede unir fragmentos incluso cuando un carácter multibyte queda dividido entre dos lecturas:

javascript— editable

Este mismo bucle es la forma en que se construyen indicadores de progreso de descarga: suma la longitud de cada fragmento y compárala con la cabecera Content-Length.

Iteración Asíncrona

En entornos modernos un ReadableStream es iterable de forma asíncrona, por lo que puedes reemplazar el bucle de lector manual con for await...of (véase iteradores y generadores asíncronos):

const response = await fetch('/large-file.txt');

for await (const chunk of response.body) {
  // chunk is a Uint8Array
  console.log('Received', chunk.length, 'bytes');
}

Esto es más limpio porque el bucle gestiona done por ti y libera el lector automáticamente. La pega es la compatibilidad: Node.js lo maneja bien, pero la iteración asíncrona directa sobre response.body aún es inconsistente entre navegadores.

Advertencia

Dado que el soporte del navegador para iterar streams de forma asíncrona es inconsistente, el bucle con getReader() sigue siendo la forma más portable. Usa for await...of en Node o cuando controlas el entorno de ejecución; recurre a un lector en código que deba ejecutarse en cualquier lugar.

Crear un ReadableStream

Puedes construir tu propia fuente pasando un objeto de fuente subyacente al constructor ReadableStream. Puede definir tres métodos opcionales:

  • start(controller) se ejecuta una vez cuando se crea el stream — ideal para la configuración inicial o para enviar datos iniciales.
  • pull(controller) se llama cada vez que el consumidor quiere más datos y la cola interna tiene espacio.
  • cancel(reason) se ejecuta si el consumidor deja de leer antes de tiempo, para que puedas limpiar los recursos.

Envías datos con controller.enqueue(chunk) y señalas el final con controller.close():

javascript— editable

Un stream puede transportar cualquier valor de JavaScript, no solo bytes — aquí emite números simples. Cuando la fuente es lenta o de extremo abierto (un WebSocket, un temporizador, datos de un sensor), coloca la lógica en pull() para que los fragmentos se produzcan solo cuando el consumidor los solicite.

TransformStream

Un TransformStream modifica los fragmentos a medida que pasan. Le proporcionas una función transform(chunk, controller) que recibe cada fragmento entrante y llama a controller.enqueue() con el resultado transformado:

const upperCaser = new TransformStream({
  transform(chunk, controller) {
    controller.enqueue(chunk.toUpperCase());
  }
});

Un transform stream expone un extremo writable (por donde entran los fragmentos) y un extremo readable (por donde salen), que es precisamente lo que hace posible el piping.

La plataforma incluye varios transforms predefinidos para que raramente tengas que escribir lógica a nivel de bytes manualmente:

  • TextDecoderStream / TextEncoderStream convierten entre fragmentos de bytes y fragmentos de texto.
  • CompressionStream / DecompressionStream aplican gzip o deflate al vuelo.

Conectar Streams con Pipes

En lugar de conectar lectores y escritores manualmente, puedes conectar streams directamente. Hay dos métodos:

  • readable.pipeTo(writable) envía cada fragmento de un readable stream a un writable stream y resuelve una promesa cuando termina.
  • readable.pipeThrough(transformStream) hace pasar los datos por un transform y devuelve un nuevo readable stream — perfecto para encadenar.

Combinar pipeThrough con TextDecoderStream te da fragmentos de texto directamente desde una respuesta de red, sin necesidad de gestionar manualmente un decoder:

const response = await fetch('/large-file.txt');
const textStream = response.body.pipeThrough(new TextDecoderStream());

for await (const textChunk of textStream) {
  console.log(textChunk); // already a string
}

Puedes encadenar tantas etapas como desees — por ejemplo response.body.pipeThrough(new DecompressionStream('gzip')).pipeThrough(new TextDecoderStream()) para descomprimir y decodificar en un pipeline declarativo.

Backpressure

Una ventaja clave de los streams frente al almacenamiento en buffer es la backpressure. Cuando el consumidor es lento, el stream señala automáticamente a la fuente que pause la producción y reanuda una vez que la cola se vacía. Con pipeTo y pipeThrough esto ocurre automáticamente — una descarga rápida no superará una escritura en disco lenta ni consumirá memoria excesiva.

Información

La backpressure es la razón por la que hacer streaming de un archivo de varios gigabytes usa solo una cantidad pequeña y acotada de memoria. El productor nunca avanza más de unos pocos fragmentos por delante del consumidor, sin importar el tamaño total de la carga útil.

Casos de Uso

Los streams destacan siempre que los datos son grandes, lentos o continuos:

  • Renderizado progresivo — muestra el inicio de una respuesta grande mientras el resto aún está llegando, en lugar de quedarse mirando una pantalla en blanco.
  • Descargas y subidas con progreso — mide los bytes a medida que fluyen para controlar una barra de progreso.
  • Procesamiento de archivos grandes — maneja un archivo fragmento a fragmento para que el uso de memoria se mantenga estable incluso con archivos mayores que la RAM.
  • Pipelines de compresión — conecta CompressionStream o DecompressionStream para comprimir datos en streaming con gzip.

Compatibilidad con Navegadores y Entornos

ReadableStream, WritableStream y TransformStream son compatibles con todos los navegadores modernos y con Node.js (donde también se exponen a través de node:stream/web). Los aspectos a vigilar son las incorporaciones más recientes: la iteración asíncrona sobre response.body y CompressionStream llegaron más tarde, así que comprueba la compatibilidad o proporciona un fallback con getReader() cuando necesites amplia cobertura. Los streams están estrechamente relacionados con los Blobsblob.stream() devuelve un ReadableStream, lo que te permite integrar objetos similares a archivos en un pipeline de streaming.

Pon a Prueba tu Conocimiento

Práctica
¿Qué propiedad de un fetch Response es un ReadableStream?
¿Qué propiedad de un fetch Response es un ReadableStream?
Práctica
¿A qué resuelve una llamada reader.read() cuando el stream ha terminado?
¿A qué resuelve una llamada reader.read() cuando el stream ha terminado?
Práctica
¿Qué método hace pasar un readable stream por un transform y devuelve un nuevo readable stream?
¿Qué método hace pasar un readable stream por un transform y devuelve un nuevo readable stream?
Was this page helpful?