4 Punkte von GN⁺ 2026-02-28 | 1 Kommentare | Auf WhatsApp teilen
  • Der Web-Streams-Standard wurde für konsistentes Daten-Streaming zwischen Browser und Server entwickelt, leidet derzeit jedoch unter Komplexität und Leistungsgrenzen, die die Developer Experience verschlechtern
  • Die bestehende API verursacht durch Designbeschränkungen wie Lock-Verwaltung, BYOB und Backpressure unnötige Last sowohl bei der Nutzung als auch bei der Implementierung
  • Cloudflare schlägt ein neues Stream-Modell auf Basis von asynchroner Iteration (async iteration) vor, das eine 2- bis bis zu 120-mal höhere Performance zeigt
  • Die neue API steigert Effizienz und Konsistenz durch eine einfache async-iterable-Struktur, explizite Backpressure-Richtlinien und Unterstützung für synchrone/asynchrone Parallelität
  • Dieser Ansatz könnte ein einheitliches Streaming-Modell für Node.js, Deno, Bun, Browser und andere Laufzeiten ermöglichen und als Ausgangspunkt für künftige Standardisierungsdiskussionen dienen

Strukturelle Grenzen von Web Streams

  • Der WHATWG-Streams-Standard wurde 2014–2016 entwickelt und browserzentriert entworfen; da es damals noch keine async iteration gab, wurde ein separates Reader-/Writer-Modell eingeführt
    • Dadurch entstanden unnötige Abläufe wie Lock-Verwaltung, komplexe Lese-Schleifen und BYOB-Pufferbehandlung
  • Das Locking-Modell belegt Streams exklusiv und verhindert parallelen Konsum; wird releaseLock() vergessen, kann der Stream dauerhaft gesperrt bleiben
  • Die Funktion BYOB (Bring Your Own Buffer) zielte auf Speicherwiederverwendung, ist wegen eines komplexen Modells zur Trennung und Übertragung von Puffern in der Praxis jedoch wenig genutzt und schwer zu implementieren
  • Backpressure wird theoretisch unterstützt, ist aber strukturell nicht real steuerbar, da etwa enqueue() selbst bei negativem desiredSize erfolgreich ist
  • Bei jedem read()-Aufruf wird die Erstellung eines Promise erzwungen, was bei hochfrequentem Streaming Leistungseinbußen und GC-Last verursacht

Probleme aus der Praxis

  • Wenn der Response-Body von fetch() nicht konsumiert wird, kann es zur Erschöpfung des Connection-Pools kommen; bei Verwendung von tee() entsteht unbegrenztes Memory-Buffering
  • TransformStream verarbeitet sofort und unabhängig davon, ob gelesen werden kann, was bei langsamen Konsumenten zu einer explodierenden Puffergröße führt
  • Beim Server-Side Rendering (SSR) verursacht die Verarbeitung tausender kleiner Chunks GC-Thrashing, wodurch die Performance stark einbricht
  • Einzelne Laufzeiten (Node.js, Deno, Bun, Workers) haben zur Entschärfung nicht standardisierte Optimierungspfade eingeführt, was wiederum Kompatibilität und Konsistenz verschlechtert
  • Die Web Platform Tests verlangen mehr als 70 komplexe Testdateien – ein Ergebnis von übermäßigem internem Zustandsmanagement und unintuitivem Verhalten

Designprinzipien der neuen Streams-API

  • Streams werden als einfache async iterables definiert und können direkt mit for await...of konsumiert werden
  • Es wird eine Pull-through-Transformation verwendet, bei der Verarbeitung nur dann stattfindet, wenn der Konsument Daten anfordert
  • Explizite Backpressure-Richtlinien (strict, block, drop-oldest, drop-newest) verhindern außer Kontrolle geratenen Speicherverbrauch
  • Daten werden als Batch-Chunks (Uint8Array[]) übergeben, um die Kosten der Promise-Erzeugung zu senken
  • Die API wird auf ausschließliche Byte-Verarbeitung vereinfacht; BYOB und komplexe Controller-Konzepte entfallen
  • Durch Unterstützung synchroner Pfade wird bei CPU-zentrierten Workloads der Promise-Overhead entfernt

Beispiele und Eigenschaften der neuen API

  • Mit Stream.push() lässt sich einfach ein Writer-/Readable-Paar erzeugen, und mit Stream.text() kann der gesamte Text gesammelt werden
  • Stream.pull() baut eine lazy Pipeline auf, die nur beim Konsum ausgeführt wird
  • Stream.share() und Stream.broadcast() unterstützen explizites Management mehrerer Konsumenten
  • Eine kombinierte Sync/Async-API (Stream.pullSync(), Stream.textSync()) maximiert die Performance bei Operationen ohne I/O
  • Für die Interoperabilität mit Web Streams ist eine Konvertierung über einfache Adapter-Funktionen möglich

Performance-Vergleich und Ausblick

  • Benchmarks unter Node.js zeigen bis zu 80- bis 90-mal, im Browser über 100-mal schnellere Verarbeitung
    • Beispiel: 275GB/s gegenüber 3GB/s in einer dreistufigen Transformationskette
  • Die Leistungssteigerung beruht auf dem Wegfall asynchronen Overheads, Batch-Verarbeitung und einem Pull-basierten Design
  • Die Implementierung wurde vollständig in reinem TypeScript/JavaScript geschrieben; bei einer nativen Implementierung sind weitere Verbesserungen möglich
  • Cloudflare präsentiert diesen Ansatz als Ausgangspunkt für die Standardisierungsdiskussion und bittet die Entwickler-Community um Feedback

Fazit

  • Web Streams waren unter den damaligen Einschränkungen sinnvoll, passen jedoch nicht zu den Sprachfunktionen und Entwicklungsmustern des modernen JavaScript
  • Das neue Modell auf Basis von async iterables vereint Einfachheit, Performance und explizite Kontrolle und zeigt das Potenzial für ein konsistentes Streaming-Ökosystem über Laufzeiten hinweg
  • Cloudflare veröffentlicht auf GitHub unter jasnell/new-streams Referenzimplementierung, Dokumentation und Beispielcode
  • Ziel ist nicht die sofortige Einführung eines neuen Standards, sondern ein praktischer Ausgangspunkt für die Diskussion über eine „bessere Streams-API“

1 Kommentare

 
GN⁺ 2026-02-28
Hacker-News-Kommentare
  • Ich habe selbst ein besseres Stream-Interface entworfen als die in diesem Artikel vorgeschlagene API
    Der bestehende Vorschlag hat die Form async iterator of UInt8Array, ich schlage dagegen eine Struktur vor, bei der next() sowohl synchrone als auch asynchrone Ergebnisse zurückgeben kann
    Dadurch
    lässt sich die Iteration im Vergleich zur bestehenden Struktur einfacher mit einem einzigen Iterator durchführen
    wenn man auf synchrone Eingaben synchrone Transformationen anwendet, kann die gesamte Verarbeitung synchron erfolgen, was Codeduplizierung reduziert
    es werden weniger unnötige Promises erzeugt, was die Performance verbessert
    Concurrency-Kontrolle wird möglich, wodurch die Grenzen von Async-Iteratoren überwunden werden können

    • Du sagst, dein Vorschlag sei besser, aber ich denke eigentlich, dass der Ansatz der anderen als grundlegendere primitive Form überlegen ist
      Mit deinem Ansatz lässt sich ihre Struktur nicht einfach bauen, umgekehrt aber schon
      Ein I/O-zentrierter Iterator muss Chunks in Einheiten von T zurückgeben, um Pufferverschwendung zu vermeiden
    • Das vorgeschlagene Stream-Konzept ist interessant, aber ihr Entwurf setzt AsyncIterator-Kompatibilität voraus
      Der Grund für die Verwendung von Uint8Array ist die Anpassung an Byte-Streams auf OS-Ebene
      Tatsächlich ist eine solche Struktur auch in C-basierten Projekten am effizientesten, daher ist es natürlich, Protokolle mit Typinformationen darauf aufzubauen
    • Ich habe in Node 24 den Geschwindigkeitsunterschied zwischen synchronen Funktionsaufrufen und async-Funktionsaufrufen per Microbenchmark gemessen, und async war etwa 90-mal langsamer
      In früheren Versionen lag der Unterschied bei bis zu 105-mal
      Ich erinnere mich, dass es in Node 16 Optimierungen für async-Verarbeitung gab und damals einige Tests kaputtgingen
    • Der Typ Uint8Array existiert nicht
      Uint8Array ist einfach ein primitiver Typ zur Darstellung eines Byte-Arrays, und Typinformationen sollten auf Anwendungsebene statt auf Protokollebene behandelt werden
    • Diese Struktur ähnelt dem Konzept der Transducer in Clojure
      Siehe: Clojure-Transducers-Dokumentation
  • Async Iterable ist ebenfalls keine perfekte Lösung
    Promise- und Stack-Switching-Overhead sind hoch, sodass die Performance bei kleinen Dateneinheiten schlecht ist
    In Lit-SSR wurde zur Lösung dessen ein Ansatz verwendet, bei dem Thunk-Funktionen in ein synchrones Iterable eingebettet werden
    Nur wenn async-Arbeit nötig ist, wird der Thunk aufgerufen und awaited, wodurch sich die SSR-Performance um das 12- bis 18-Fache verbessert hat
    Da die Streams API jedoch schwer ein solches fragiles Vertragsmodell übernehmen kann, halte ich eine Struktur wie write() und writeAsync() mit optionaler asynchroner Verarbeitung für ideal

    • Das von dir angesprochene Problem kann mein Stream-Iterator lösen
      Ich habe ein Beispiel mit einem synchronen Generator als GitHub-Code geteilt
      Der Kern ist der Teil step.value.then(value => this.next(value))
    • Mir gefällt der Vorschlag von conartist6 (next(): {done, value: T} | Promise)
      Seit der Debatte „Do not unleash Zalgo“ von 2013 gibt es die Tendenz, Formen wie MaybeAsync zu vermeiden
      Ich denke aber, dass diese Angst übertrieben ist und schnelles, flexibles API-Design behindert
      Man kann auch Utilities bauen, die mehrere Werte auf einmal holen, und das Geschwindigkeitsproblem von Generatoren ist in der Praxis wohl nicht so groß
  • In Node.js mit Web Streams zu arbeiten ist schmerzhaft
    Sie wurden browserzentriert entworfen und sind in Server-Umgebungen unpraktisch
    Selbst für einfache Transformationen muss man einen Transform-Stream umhüllen, und intuitives Chaining wie mit .pipe() ist schwierig
    Der Ansatz über Async Iterable ist viel natürlicher und passt gut zu for-await-of
    Die Web-Streams-Spezifikation ist zu abstraktionszentriert und dadurch wenig praxisnah

    • Es überrascht mich, dass es überhaupt Leute gibt, die Web Streams in Node tatsächlich verwenden
      Ich dachte, das sei einfach nur für Client-Server-Kompatibilität da
  • Der eigentliche Vorteil ist nicht nur Performance, sondern auch Konsistenz zwischen Umgebungen (convergence)
    Wenn ReadableStream in Browsern, Workern und anderen Runtimes gleich funktioniert,
    verbessert das die Portabilität des Codes und reduziert Backpressure-Bugs
    Die Standardisierung der Stream-Schicht ist entscheidend für den Aufbau zuverlässiger Streaming-Systeme

    • Genau, es geht nicht nur um Performance, sondern stark um den Wert der Standardisierung
  • Ich habe früher eine Abstraktion namens Repeater gebaut
    Das ist das Konzept des Promise-Konstruktors, übertragen auf Async Iterable, mit Steuerung von Events über push/stop
    Die Repeater-Bibliothek ist stabil genug, um auf 6,5 Millionen Downloads pro Woche zu kommen
    In letzter Zeit bevorzuge ich eher Streams, aber die Kritik rund um tee() ist weiterhin gültig
    Ich denke, die Richtung Async Iterable als grundlegende Abstraktion ist richtig

    • Ich fand interessant, dass stop bei Repeater zugleich als Funktion und als Promise funktioniert
      Nachdem ich den Quellcode gesehen habe,
      dachte ich, dass das zwar vom traditionellen Muster abweicht, aber eine bewusste Wahl für ergonomisches Design sein könnte
    • Hat zwar nichts mit dem Thema zu tun, aber ich habe mich sehr über das Beispiel mit dem Konami-Code gefreut
      Ich bin so nostalgisch, dass ich sogar „Up, Up, Down, Down, Left, Right, Left, Right, B, A“ in meine E-Mail-Signatur schreiben würde
  • Ich habe auch einmal einen Wrapper gebaut, um AsyncIterable kompakter zu verwenden
    fluent-async-iterator war nützlich für kleines Daten-Streaming in Lambda- oder CLI-Pipelines
    Ich hatte gehofft, dass es inzwischen eine bessere API gäbe

  • Das Backpressure-Verhalten von ReadableStream.tee() ist verwirrend, weil es dem von pipe() in Node.js entgegengesetzt ist
    In der Spezifikation heißt es, „der langsamste Ausgang sollte das Tempo bestimmen“, aber in der tatsächlichen Implementierung blockiert es, selbst wenn die schnelle Seite nicht konsumiert wird
    Ich denke, eine knappe Push-basierte Struktur wie in einer neuen Stream-API wäre besser
    Node und Web Streams verwenden unendliche Queues, sodass man synchron massenhaft res.write() aufrufen kann,
    aber diese API erzwingt einen yield-Fluss auf Generator-Basis und ist dadurch sicherer

  • Dass bei der Verwendung von undici(fetch) in Node.js Probleme mit erschöpften Connection-Pools auftreten,
    liegt an den Grenzen einer Garbage-Collection-Sprache
    Wenn Ressourcen nicht explizit geschlossen werden, kommt es je nach GC-Timing zu Lecks
    Der RAII-Ansatz (reference counting) von C++ ist eher sicherer

  • Was das Freigeben von Ressourcen angeht, hoffe ich, dass sich das Muster using/await using weiter verbreitet
    Ich wende gerade eine Struktur mit Unterstützung für dispose/disposeAsync wie bei using in C# auf DB-Treiber an

  • Benchmark-Zahlen wie 530GB/s überschreiten die Speicherbandbreite des M1 Pro (200GB/s) und sind daher schwer glaubwürdig
    Es ist gut möglich, dass es sich um einen vibe-codierten Benchmark mit unzureichender Qualitätssicherung der Implementierung handelt