Back

Streams Explicados para Desarrolladores Web

Streams Explicados para Desarrolladores Web

Cuando llamas a fetch() y esperas una respuesta, el navegador ya ha estado recibiendo esos datos en fragmentos. La API de Web Streams le da a tu código JavaScript acceso a esos fragmentos a medida que llegan, en lugar de esperar a que la respuesta completa aterrice antes de poder manipularla.

Ese cambio — de “esperar por todo” a “procesar a medida que llega” — es de lo que tratan los streams.

Puntos Clave

  • La API de Web Streams te permite procesar datos de forma incremental a medida que llegan, en lugar de almacenar respuestas completas en memoria.
  • ReadableStream, WritableStream y TransformStream son los tres primitivos fundamentales — bloques de construcción componibles para pipelines de datos.
  • response.body de fetch() es el punto de entrada más común: un ReadableStream que puedes leer fragmento por fragmento.
  • Usa pipeThrough() y pipeTo() para encadenar transformaciones y salidas juntas, con manejo automático de contrapresión incorporado.

Por Qué Cargar Todo de una Vez Es un Problema

El enfoque tradicional para obtener datos se ve así:

const response = await fetch('/large-dataset.json')
const data = await response.json()
// Nothing happens until all bytes are downloaded and parsed

Para cargas pequeñas, esto está bien. Para un archivo JSON de 50MB o una respuesta de API de larga duración, estás manteniendo todo en memoria antes de procesar un solo registro. En dispositivos con recursos limitados o conexiones lentas, eso significa interfaces lentas, alta presión de memoria y usuarios frustrados.

Los streams te permiten comenzar a trabajar con los datos en el momento en que llega el primer fragmento.

Los Tres Primitivos Fundamentales de la API de Web Streams

La API de Web Streams está construida alrededor de tres clases:

  • ReadableStream — una fuente desde la cual lees datos
  • WritableStream — un destino hacia el cual escribes datos
  • TransformStream — se sitúa en el medio, leyendo de un lado y escribiendo datos transformados al otro

Los datos se mueven a través de estos streams en chunks (fragmentos) — pequeñas piezas procesadas una a la vez. Un chunk puede ser un Uint8Array de bytes, una cadena de texto, o cualquier valor de JavaScript, dependiendo del stream.

Fetch Streaming: Leyendo una Respuesta de Forma Incremental

La mayoría de las respuestas de fetch() exponen su cuerpo como un ReadableStream a través de response.body. Este es el punto de entrada más común a los streams de JavaScript para desarrolladores frontend.

async function processLargeResponse(url) {
  const response = await fetch(url)
  const reader = response.body.getReader()
  const decoder = new TextDecoder()

  try {
    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      console.log(decoder.decode(value, { stream: true }))
    }
  } finally {
    reader.releaseLock()
  }
}

reader.read() devuelve una promesa que se resuelve con { value, done }. Cuando done es true, el stream ha terminado. Este patrón te permite procesar una respuesta de múltiples megabytes fragmento por fragmento, sin almacenar todo en buffer.

Nota sobre streaming de cuerpos de solicitud: Pasar un ReadableStream como cuerpo de solicitud de fetch() es posible pero tiene soporte desigual en navegadores. El streaming de respuestas es el patrón práctico y bien soportado al que recurrir hoy en día.

Construyendo Pipelines de Datos con pipeThrough() y pipeTo()

Donde los streams se vuelven genuinamente poderosos es en la composición. Puedes encadenar un ReadableStream a través de una o más instancias de TransformStream y canalizar el resultado hacia un WritableStream.

fetch('./data.txt').then((response) =>
  response.body
    .pipeThrough(new TextDecoderStream())
    .pipeThrough(new TransformStream({
      transform(chunk, controller) {
        controller.enqueue(chunk.toUpperCase())
      }
    }))
    .pipeTo(new WritableStream({
      write(chunk) {
        document.body.textContent += chunk
      }
    }))
)

Este pipeline decodifica bytes a texto, transforma cada chunk a mayúsculas, luego lo escribe en el DOM — todo de forma incremental, sin esperar la respuesta completa.

pipeThrough() conecta un ReadableStream a un TransformStream y devuelve un nuevo ReadableStream. pipeTo() conecta un ReadableStream a un WritableStream y devuelve una promesa que se resuelve cuando el stream se completa.

Contrapresión: Cómo los Streams Evitan la Sobrecarga

Cuando un consumidor procesa datos más lentamente de lo que un productor los genera, los streams aplican contrapresión (backpressure) — una señal que se propaga hacia atrás a través de la cadena de pipes, indicándole a la fuente que reduzca la velocidad. Esto ocurre automáticamente cuando usas pipeTo() y pipeThrough(). Es una de las principales razones para preferir el piping sobre leer fragmentos manualmente en un bucle.

Streams Integrados que Vale la Pena Conocer

El navegador incluye varias utilidades de stream listas para usar:

  • TextDecoderStream / TextEncoderStream — convierten entre bytes y cadenas de texto
  • CompressionStream / DecompressionStream — comprimen o descomprimen datos al vuelo con gzip o deflate
  • Blob.stream() — lee cualquier Blob o File como un ReadableStream

Las versiones modernas de Node.js también soportan la API de Web Streams, por lo que los pipelines que construyes para el navegador se transfieren limpiamente a entornos del lado del servidor.

Conclusión

La API de Web Streams proporciona a los desarrolladores frontend una forma componible y eficiente en memoria de manejar datos que llegan a lo largo del tiempo. ReadableStream y TransformStream son los primitivos que más usarás — especialmente cuando se combinan con fetch() para procesamiento incremental de respuestas. Comienza con response.body, recurre a pipeThrough() cuando necesites transformar datos, y deja que la contrapresión maneje el control de flujo por ti.

Preguntas Frecuentes

Sí. ReadableStream, WritableStream, TransformStream y los métodos de piping están soportados en todos los navegadores modernos incluyendo Chrome, Firefox, Safari y Edge. El streaming de cuerpos de respuesta de fetch a través de response.body también está ampliamente soportado. Los cuerpos de solicitud con streaming en fetch tienen soporte más limitado, así que verifica las tablas de compatibilidad antes de depender de esa característica.

Si cualquier etapa en una cadena de pipes lanza un error, el error se propaga a través del pipeline. El lado legible se vuelve erróneo y el lado escribible se aborta. Puedes manejar esto pasando un objeto de opciones con una señal o capturando la promesa devuelta por pipeTo. Para bucles de lectura manual, envuelve tus llamadas de lectura en bloques try-catch.

Node.js originalmente incluyó su propia API de streams con clases Readable, Writable y Transform. La API de Web Streams es un estándar separado diseñado para navegadores. Las versiones modernas de Node.js soportan ambas. La API de Web Streams usa un modelo basado en pull con promesas, mientras que los streams clásicos de Node usan un modelo push basado en eventos. El código escrito contra la API de Web Streams es portable entre entornos de navegador y servidor.

Si la respuesta es pequeña, digamos menos de unos pocos cientos de kilobytes, almacenar en buffer con response.json o response.text es más simple y perfectamente eficiente. Los streams agregan valor cuando se manejan cargas grandes, datos en tiempo real, o situaciones donde quieres mostrar resultados parciales antes de que llegue la respuesta completa. Para llamadas API sencillas que devuelven JSON compacto, el enfoque tradicional está bien.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay