W3docs

JavaScript Streams API

Lerne die JavaScript Streams API — Daten schrittweise lesen mit ReadableStream, schreiben mit WritableStream, transformieren mit TransformStream und Streams zu effizienten Pipelines verketten.

Die Streams API ermöglicht es, Daten in kleinen Stücken zu verarbeiten, sobald sie eintreffen, anstatt alles auf einmal in den Speicher zu laden. Dies ist unverzichtbar bei der Arbeit mit großen Dateien, langsamen Netzwerkantworten und Echtzeitdaten: Man kann mit der Verarbeitung der ersten Bytes beginnen, während der Rest noch übertragen wird, und muss nie die gesamte Nutzlast im Speicher halten.

Die API basiert auf drei grundlegenden Typen. Ein ReadableStream ist eine Quelle, aus der Daten gezogen werden. Ein WritableStream ist eine Senke, in die Daten geschrieben werden. Ein TransformStream sitzt in der Mitte und nimmt Chunks an einem Ende entgegen, während er modifizierte Chunks am anderen Ende ausgibt. Wenn man diese drei versteht, lassen sie sich zu effizienten Pipelines zusammensetzen.

Einen Stream lesen

Der häufigste Weg, einen Stream zu erhalten, ist die Fetch API. Ein Response-Objekt stellt seinen Körper als ReadableStream über response.body bereit, sodass man den Download Chunk für Chunk verarbeiten kann, anstatt mit response.text() auf das vollständige Ergebnis zu warten.

Um manuell zu lesen, ruft man getReader() auf, um einen Reader an den Stream zu binden, und iteriert dann in einer Schleife über reader.read(). Jeder Aufruf löst sich zu einem Objekt mit done und value auf:

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');
}

Jedes value ist ein Uint8Array — ein Chunk roher Bytes, kein string (siehe Typed arrays). Wenn done den Wert true hat, ist der Stream beendet und value ist undefined. Um die Bytes in Text umzuwandeln, verwendet man typischerweise einen TextDecoder, der Chunks zusammenfügen kann, selbst wenn ein Multibyte-Zeichen auf zwei Lesevorgänge aufgeteilt ist:

javascript— editable

Diese gleiche Schleife ist die Grundlage für Download-Fortschrittsanzeigen: Man summiert die Länge jedes Chunks und vergleicht sie mit dem Content-Length-Header.

Async-Iteration

In modernen Umgebungen ist ein ReadableStream async-iterierbar, sodass man die manuelle Reader-Schleife durch for await...of ersetzen kann (siehe async-Iteratoren und -Generatoren):

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

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

Das ist übersichtlicher, weil die Schleife done selbst behandelt und den Reader automatisch freigibt. Der Nachteil ist die Unterstützung: Node.js verarbeitet dies gut, aber die direkte async-Iteration über response.body ist in Browsern noch uneinheitlich.

Warnung

Da die Browser-Unterstützung für die async-Iteration von Streams inkonsistent ist, bleibt die getReader()-Schleife die portabelste Form. Verwende for await...of in Node.js oder wenn du die Laufzeitumgebung kontrollierst; greife auf einen Reader zurück, wenn der Code überall laufen muss.

Einen ReadableStream erstellen

Man kann eine eigene Quelle erstellen, indem man ein underlying source-Objekt an den ReadableStream-Konstruktor übergibt. Es kann drei optionale Methoden definieren:

  • start(controller) wird einmalig beim Erstellen des Streams ausgeführt — gut für die Einrichtung oder das Senden erster Daten.
  • pull(controller) wird aufgerufen, wenn der Konsument mehr Daten möchte und die interne Warteschlange Platz hat.
  • cancel(reason) wird ausgeführt, wenn der Konsument das Lesen vorzeitig abbricht, sodass aufgeräumt werden kann.

Daten werden mit controller.enqueue(chunk) hinzugefügt und das Ende wird mit controller.close() signalisiert:

javascript— editable

Ein Stream kann jeden JavaScript-Wert transportieren, nicht nur Bytes — hier werden schlichte Zahlen ausgegeben. Wenn die Quelle langsam oder offen ist (ein WebSocket, ein Timer, Sensordaten), sollte die Logik in pull() platziert werden, damit Chunks nur dann produziert werden, wenn der Konsument sie anfordert.

TransformStream

Ein TransformStream verändert Chunks beim Durchlaufen. Man gibt ihm eine transform(chunk, controller)-Funktion, die jeden eingehenden Chunk empfängt und controller.enqueue() mit dem transformierten Ergebnis aufruft:

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

Ein Transform-Stream stellt ein writable-Ende (wo Chunks hineingehen) und ein readable-Ende (wo sie herauskommen) bereit, was genau das ist, was Piping ermöglicht.

Die Plattform liefert mehrere vorgefertigte Transforms mit, sodass man selten Byte-Level-Logik von Hand schreiben muss:

  • TextDecoderStream / TextEncoderStream konvertieren zwischen Byte-Chunks und Text-Chunks.
  • CompressionStream / DecompressionStream wenden gzip oder deflate im laufenden Betrieb an.

Streams zusammenschalten

Anstatt Reader und Writer manuell zu verdrahten, können Streams direkt verbunden werden. Es gibt zwei Methoden:

  • readable.pipeTo(writable) sendet jeden Chunk eines lesbaren Streams in einen schreibbaren Stream und löst ein Promise auf, wenn dies abgeschlossen ist.
  • readable.pipeThrough(transformStream) leitet die Daten durch einen Transform und gibt einen neuen lesbaren Stream zurück — perfekt für Verkettungen.

Die Kombination von pipeThrough mit TextDecoderStream liefert Text-Chunks direkt aus einer Netzwerkantwort, ohne manuelles Decoder-Bookkeeping:

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
}

Man kann beliebig viele Stufen verketten — zum Beispiel response.body.pipeThrough(new DecompressionStream('gzip')).pipeThrough(new TextDecoderStream()), um in einer deklarativen Pipeline zu dekomprimieren und zu dekodieren.

Backpressure

Ein wesentlicher Vorteil von Streams gegenüber dem vollständigen Puffern ist Backpressure. Wenn der Konsument langsam ist, signalisiert der Stream der Quelle automatisch, die Produktion zu pausieren, und setzt sie fort, sobald die Warteschlange sich leert. Mit pipeTo und pipeThrough geschieht das automatisch — ein schneller Download überläuft keinen langsamen Festplattenschreibvorgang und bläst den Speicher nicht auf.

Info

Backpressure ist der Grund, warum das Streamen einer mehrere Gigabyte großen Datei nur eine kleine, begrenzte Menge Speicher verbraucht. Der Produzent ist dem Konsumenten niemals mehr als ein paar Chunks voraus, unabhängig davon, wie groß die gesamte Nutzlast ist.

Anwendungsfälle

Streams glänzen immer dann, wenn Daten groß, langsam oder kontinuierlich sind:

  • Progressives Rendering — zeige den Anfang einer großen Antwort an, während der Rest noch eingetroffen, anstatt auf einen leeren Bildschirm zu starren.
  • Downloads und Uploads mit Fortschritt — messe Bytes im Fluss, um eine Fortschrittsanzeige anzutreiben.
  • Große Dateien verarbeiten — verarbeite eine Datei Chunk für Chunk, sodass der Speicherverbrauch konstant bleibt, selbst bei Dateien, die größer als der RAM sind.
  • Kompressions-Pipelines — leite Daten durch CompressionStream oder DecompressionStream, um Daten beim Streamen zu gzippen.

Browser- und Umgebungsunterstützung

ReadableStream, WritableStream und TransformStream werden in allen modernen Browsern und in Node.js unterstützt (wo sie auch über node:stream/web verfügbar sind). Auf neuere Ergänzungen ist zu achten: Async-Iteration über response.body und CompressionStream kamen später hinzu, also sollte man die Unterstützung prüfen oder einen getReader()-Fallback bereitstellen, wenn breite Abdeckung erforderlich ist. Streams stehen in enger Beziehung zu Blobsblob.stream() gibt einen ReadableStream zurück und ermöglicht es, dateiähnliche Objekte in eine Streaming-Pipeline einzubinden.

Teste dein Wissen

Übung
Welche Eigenschaft einer fetch-Response ist ein ReadableStream?
Welche Eigenschaft einer fetch-Response ist ein ReadableStream?
Übung
Wozu löst sich ein reader.read()-Aufruf auf, wenn der Stream beendet ist?
Wozu löst sich ein reader.read()-Aufruf auf, wenn der Stream beendet ist?
Übung
Welche Methode leitet einen lesbaren Stream durch einen Transform und gibt einen neuen lesbaren Stream zurück?
Welche Methode leitet einen lesbaren Stream durch einen Transform und gibt einen neuen lesbaren Stream zurück?
Was this page helpful?