4 puntos por GN⁺ 2026-02-28 | 1 comentarios | Compartir por WhatsApp
  • El estándar Web Streams fue diseñado para un streaming de datos consistente entre navegador y servidor, pero actualmente la complejidad y las limitaciones de rendimiento deterioran la experiencia de desarrollo
  • La API existente genera cargas innecesarias tanto en uso como en implementación debido a restricciones de diseño como gestión de locks, BYOB y backpressure
  • Cloudflare propone un nuevo modelo de streams basado en iteración asíncrona (async iteration), y este enfoque muestra un rendimiento 2 veces hasta 120 veces más rápido
  • La nueva API mejora la eficiencia y la consistencia mediante una estructura simple de async iterable, políticas explícitas de backpressure y soporte paralelo para sincronía/asíncronía
  • Este enfoque puede habilitar un modelo de streaming unificado en todos los runtimes, como Node.js, Deno, Bun y navegadores, y podría convertirse en el punto de partida para futuras discusiones de estandarización

Limitaciones estructurales de Web Streams

  • El estándar WHATWG Streams fue desarrollado entre 2014 y 2016 con un diseño centrado en el navegador, y como en ese momento no existía la async iteration, se introdujo un modelo separado de reader/writer
    • Esto generó procedimientos innecesarios como gestión de locks, bucles de lectura complejos y manejo de buffers BYOB
  • El modelo de locking ocupa el stream de forma exclusiva e impide el consumo en paralelo, y si se omite releaseLock(), puede ocurrir que el stream quede bloqueado permanentemente
  • La función BYOB (Bring Your Own Buffer) buscaba reutilizar memoria, pero debido a un modelo complejo de separación y transferencia de buffers, su uso real es bajo y su implementación difícil
  • El backpressure está soportado en teoría, pero su estructura no permite un control real, por ejemplo, enqueue() sigue teniendo éxito incluso cuando desiredSize es negativo
  • En cada llamada a read() se fuerza la creación de un Promise, lo que en streams de alta frecuencia provoca caída de rendimiento y carga de GC

Problemas visibles en la práctica

  • Si no se consume el cuerpo de respuesta de fetch(), se produce agotamiento del pool de conexiones, y al usar tee() aparece buffering ilimitado en memoria
  • TransformStream procesa de inmediato sin importar si la lectura está lista, lo que en entornos con consumidores lentos provoca crecimiento explosivo del buffer
  • En server-side rendering (SSR), el manejo de miles de chunks pequeños provoca GC thrashing, lo que reduce drásticamente el rendimiento
  • Cada runtime (Node.js, Deno, Bun, Workers) introdujo rutas de optimización no estándar para mitigar esto, pero eso terminó deteriorando la compatibilidad y la consistencia
  • Web Platform Tests exige más de 70 archivos de prueba complejos, lo que es resultado de un manejo excesivo del estado interno y comportamientos poco intuitivos

Principios de diseño de la nueva API de streams

  • El stream se define como un simple async iterable, por lo que puede consumirse directamente con for await...of
  • Se adopta una transformación pull-through, de modo que el procesamiento solo ocurre cuando el consumidor solicita datos
  • Se ofrecen políticas explícitas de backpressure (strict, block, drop-oldest, drop-newest) para evitar explosiones de memoria
  • Los datos se entregan en unidades de chunks por lote (Uint8Array[]) para reducir el costo de crear Promises
  • Se simplifica al enfocarse exclusivamente en procesamiento de bytes, eliminando BYOB y conceptos complejos de controlador
  • El soporte para rutas sincrónicas elimina el overhead de Promise en tareas centradas en CPU

Ejemplos y características de la nueva API

  • Con Stream.push() se puede crear fácilmente un par writer/readable, y con Stream.text() se puede recopilar todo el texto
  • Stream.pull() construye un pipeline perezoso (lazy) que solo se ejecuta en el momento del consumo
  • Stream.share() y Stream.broadcast() permiten una gestión explícita de múltiples consumidores
  • La API Sync/Async (Stream.pullSync(), Stream.textSync()) maximiza el rendimiento en operaciones sin I/O
  • Para interoperar con Web Streams, se puede convertir mediante funciones adaptadoras simples

Comparación de rendimiento y perspectivas

  • En benchmarks sobre Node.js se confirmó una velocidad de procesamiento hasta 80~90 veces mayor, y en navegadores más de 100 veces mayor
    • Ejemplo: en una cadena de transformación de 3 etapas, 275GB/s vs 3GB/s
  • La mejora de rendimiento proviene de la eliminación del overhead asíncrono, el procesamiento por lotes y el diseño basado en pull
  • Esta implementación fue escrita en TypeScript/JavaScript puro, y podría mejorar aún más con una implementación nativa
  • Cloudflare presenta este enfoque como un punto de partida para discutir estándares y pide retroalimentación de la comunidad de desarrolladores

Conclusión

  • Web Streams fue razonable dadas las limitaciones de su momento, pero no encaja con las funciones del lenguaje ni con los patrones de desarrollo del JavaScript moderno
  • El nuevo modelo basado en async iterable cumple con simplicidad, rendimiento y control explícito, y plantea la posibilidad de construir un ecosistema de streaming consistente entre runtimes
  • Cloudflare publicó la implementación de referencia, documentación y código de ejemplo en GitHub: jasnell/new-streams
  • El objetivo no es definir de inmediato un nuevo estándar, sino establecer un punto de partida práctico para debatir una “mejor API de streams”

1 comentarios

 
GN⁺ 2026-02-28
Comentarios de Hacker News
  • Diseñé directamente una interfaz de Stream mejor que la API propuesta en este artículo
    La propuesta actual tiene la forma de async iterator of UInt8Array, pero yo propongo una estructura en la que next() puede devolver resultados síncronos o asíncronos
    Esto permite
    recorrer todo de forma más simple con un único iterador en comparación con la estructura existente
    si aplicas una transformación síncrona a una entrada síncrona, todo el procesamiento puede hacerse de forma síncrona, reduciendo la duplicación de código
    se reduce la creación innecesaria de Promise, lo que mejora el rendimiento
    permite control de concurrencia, superando las limitaciones de async iterator

    • Dices que tu enfoque es mejor, pero en realidad creo que el del otro lado es superior como una forma primitiva más fundamental
      Con tu enfoque no se puede construir fácilmente su estructura, mientras que al revés sí
      Un iterador orientado a I/O debe devolver chunks en unidades de T para evitar el desperdicio de búfer
    • El concepto de stream que propones es interesante, pero su diseño parte de la compatibilidad con AsyncIterator
      Se usa Uint8Array para alinearse con los byte streams a nivel de SO
      De hecho, incluso en proyectos basados en C este tipo de estructura es la más eficiente, así que es natural que un protocolo con información de tipos se construya encima de eso
    • En Node 24 medí con microbenchmarks la diferencia de velocidad entre llamadas a funciones síncronas y async, y resultó ser unas 90 veces más lento
      En versiones anteriores la diferencia llegaba hasta 105 veces
      Recuerdo que hubo una optimización del procesamiento async en Node 16, y que en ese momento se rompieron algunas pruebas
    • El tipo Uint8Array no existe
      Uint8Array es simplemente un tipo primitivo para representar arreglos de bytes, y la información de tipos debe manejarse a nivel de aplicación, no de protocolo
    • Esta estructura se parece al concepto de transducer de Clojure
      Referencia: Documentación de Clojure Transducers
  • Async iterable tampoco es una solución perfecta
    El overhead de Promise y del cambio de stack es grande, así que el rendimiento es malo cuando se manejan datos en unidades pequeñas
    En Lit-SSR usaron un enfoque de incluir thunks dentro de un iterable síncrono para resolverlo
    Solo cuando se necesita trabajo async se invoca el thunk y se hace await, lo que mejoró el rendimiento de SSR entre 12 y 18 veces
    Aun así, como es difícil que la Streams API adopte ese tipo de contrato frágil, me parece ideal una estructura que permita asincronía opcional como write() y writeAsync()

    • El problema que mencionas lo puede resolver mi stream iterator
      Comparto un ejemplo usando un generator síncrono en este código de GitHub
      La clave está en step.value.then(value => this.next(value))
    • Me gusta la propuesta de conartist6 (next(): {done, value: T} | Promise)
      Desde el debate de 2013 sobre “Do not unleash Zalgo”, ha habido una tendencia a evitar formas como MaybeAsync, pero
      creo que ese miedo está demasiado exagerado y está bloqueando el diseño de APIs rápidas y flexibles
      También se pueden crear utilidades para traer varios valores de una vez, y siento que el problema de velocidad de los generators en la práctica no es tan grande
  • En Node.js, manejar Web Streams es doloroso
    Están diseñados pensando en el navegador, así que son incómodos en entornos de servidor
    Incluso para una transformación simple hay que envolver un transform stream, y es difícil encadenar de forma intuitiva como con .pipe()
    El enfoque de async iterable se siente mucho más natural y encaja bien con for-await-of
    La especificación de Web Streams está demasiado centrada en la abstracción, así que resulta poco práctica

    • Me sorprende que haya gente que realmente use Web Streams en Node
      Yo pensaba que eso era solo para compatibilidad entre cliente y servidor
  • La ventaja real no es solo el rendimiento, sino también la consistencia entre entornos (convergence)
    Si ReadableStream se comporta igual en navegadores, Worker y otros runtimes,
    la portabilidad del código mejora y también se reducen los bugs de backpressure
    Estandarizar la capa de streams es clave para construir sistemas de streaming confiables

    • Sí, el valor no es solo el rendimiento, sino la estandarización
  • Antes hice una abstracción llamada Repeater
    Es una idea que lleva el constructor de Promise a async iterable, controlando los eventos con push/stop
    La librería Repeater es lo bastante estable como para registrar 6.5 millones de descargas semanales
    Últimamente prefiero más los streams, pero las críticas relacionadas con tee() siguen siendo válidas
    Creo que tomar async iterable como abstracción base es la dirección correcta

    • Me pareció interesante que stop en Repeater funcione tanto como función como Promise
      Después de ver el código fuente,
      pensé que, aunque se aparta del patrón tradicional, podría ser una decisión intencional para una arquitectura ergonómica
    • No tiene que ver con el tema, pero me alegró muchísimo ver el ejemplo del código Konami
      Me da tanta nostalgia que hasta uso “Up, Up, Down, Down, Left, Right, Left, Right, B, A” en la firma del correo
  • Yo también hice alguna vez un wrapper para usar AsyncIterable de forma más concisa
    Era fluent-async-iterator,
    y me resultó útil para streaming de datos a pequeña escala en pipelines de Lambda o CLI
    Para estas fechas esperaba que ya hubiera aparecido una API mejor

  • El comportamiento de backpressure de ReadableStream.tee() es confuso porque es lo opuesto al de pipe() en Node.js
    La especificación dice que “la salida más lenta debería determinar la velocidad”, pero en la implementación real incluso el lado rápido se bloquea si el otro no consume
    Me parece mejor una estructura simple basada en push como la de la nueva Stream API
    Node y Web Streams dejan colas infinitas para que uno pueda abusar de res.write() de forma síncrona, pero
    esta API fuerza el flujo de yield basado en generators, así que es más segura

  • Que en Node.js se agote el pool de conexiones al usar undici(fetch)
    se debe a las limitaciones de los lenguajes con recolección de basura
    Si no se cierran explícitamente los recursos, se generan fugas según el momento en que corra el GC
    El enfoque de RAII (reference counting) de C++ es, de hecho, más seguro

  • En cuanto a la liberación de recursos, ojalá el patrón using/await using se siga expandiendo
    Estoy aplicando a drivers de base de datos una estructura similar al using de C# que soporta dispose/disposeAsync

  • Las cifras del benchmark (por ejemplo, 530GB/s) superan el ancho de banda de memoria del M1 Pro (200GB/s), así que cuesta creerlas
    Es muy probable que se trate de un benchmark vibe-coded con poco control de calidad en la implementación