Signatur
Beschreibung
ReadableStream ist das zentrale Interface der Streams API. Es steht für eine Datenquelle, die ihre Daten nicht auf einmal, sondern nach und nach in einzelnen Blöcken (Chunks) liefert. So lassen sich große oder langsam eintreffende Daten wie Netzwerkantworten, Dateien oder Server-Sent-Daten verarbeiten, ohne alles im Speicher zu puffern. Response.body von fetch() und Blob.prototype.stream() liefern zum Beispiel einen ReadableStream.
Einen eigenen Stream erzeugst du mit new ReadableStream(underlyingSource, queuingStrategy). Die Underlying Source legt über die Methoden start, pull und cancel fest, wie Daten in die interne Warteschlange kommen. Mit type: "bytes" entsteht ein Byte-Stream, der „Zero-Copy“-Lesen mit einem ReadableStreamBYOBReader erlaubt.
Gelesen wird über einen Reader (getReader()), mit for await...of (asynchrone Iteration) oder über Pipes: pipeTo() schreibt in einen WritableStream, pipeThrough() leitet durch einen TransformStream, etwa TextDecoderStream oder CompressionStream. Mit tee() teilst du einen Stream in zwei unabhängige Zweige auf.
Wichtige Mitglieder:
locked(Eigenschaft) – zeigt an, ob der Stream gerade an einen Reader gebunden istcancel(reason)– bricht den Stream abgetReader({ mode })– holt einen Default- oder BYOB-ReaderpipeTo(dest, options),pipeThrough(transform, options),tee()ReadableStream.from(iterable)(statisch) – erzeugt einen Stream aus einem (asynchronen) Iterable
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $underlyingSource | object | {} | Legt das Verhalten der Quelle fest: start(controller) wird einmal beim Erzeugen aufgerufen, pull(controller) immer dann, wenn die Warteschlange Nachschub braucht, cancel(reason) beim Abbruch. type: "bytes" erzeugt einen Byte-Stream, autoAllocateChunkSize steuert die automatische Puffergröße. |
| $queuingStrategy | { highWaterMark?: number, size?: (chunk) => number } | CountQueuingStrategy | ByteLengthQueuingStrategy | { highWaterMark: 1 } (bei Byte-Streams 0) | Steuert den Gegendruck (Backpressure): highWaterMark ist die gewünschte Füllhöhe der Warteschlange, size berechnet die Größe eines Chunks. Bei type: "bytes" darf size nicht angegeben werden. |
Rückgabewert
ReadableStream-Instanz, die über einen Reader, eine Pipe oder for await...of gelesen werden kann.Beispiele
Eigenen Stream erzeugen und mit einem Reader lesen
const stream = new ReadableStream({
start(controller) {
controller.enqueue('Hallo');
controller.enqueue('Streams');
controller.close();
}
});
const reader = stream.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log(value);
}
reader.releaseLock();
fetch-Antwort beim Download mitlesen (Fortschritt)
const response = await fetch('/grosse-datei.json');
const total = Number(response.headers.get('Content-Length')) || 0;
const reader = response.body.getReader();
let received = 0;
const chunks = [];
while (true) {
const { value, done } = await reader.read();
if (done) break;
chunks.push(value);
received += value.byteLength;
if (total) console.log(`Fortschritt: ${Math.round((received / total) * 100)} %`);
}
const text = new TextDecoder().decode(await new Blob(chunks).arrayBuffer());
console.log('Fertig, Zeichen:', text.length);
Text mit pipeThrough dekodieren und mit for await iterieren
const response = await fetch('/log.txt');
const textStream = response.body.pipeThrough(new TextDecoderStream());
for await (const chunk of textStream) {
console.log('Block empfangen:', chunk.length, 'Zeichen');
}
Pull-basierte Quelle mit Backpressure und Abbruch
let n = 0;
const zahlen = new ReadableStream({
pull(controller) {
// wird nur aufgerufen, wenn der Konsument Daten braucht
controller.enqueue(n++);
if (n > 1000) controller.close();
},
cancel(reason) {
console.log('Abgebrochen:', reason);
}
}, { highWaterMark: 5 });
const reader = zahlen.getReader();
console.log((await reader.read()).value);
console.log((await reader.read()).value);
await reader.cancel('genug gelesen');
// Wichtig · Fallstricke
- Sperre (Lock): Solange ein Reader aktiv ist, gilt
locked === true. Ein zweitesgetReader(),pipeTo()odertee()wirft dann einenTypeError. Gib die Sperre mitreader.releaseLock()frei. - Nur einmal lesbar: Ein Stream lässt sich nur ein einziges Mal auslesen.
Response.bodyist nachresponse.json()verbraucht. Brauchst du die Daten zweimal, nutzetee()(Vorsicht: Der langsamere Zweig puffert alles im Speicher) oderresponse.clone(). - Asynchrone Iteration (
for await...of) wird von Safari erst seit Version 18 unterstützt,ReadableStream.from()ist ebenfalls noch relativ neu. Prüfe bei Bedarf mit Feature-Detection oder setze ein Polyfill ein. for awaitbricht den Stream beibreakautomatisch ab. Mitstream.values({ preventCancel: true })bleibt er offen.- Chunks von
fetchsindUint8Arrays mit beliebigen Grenzen. Mehrbyte-UTF-8-Zeichen können auf zwei Chunks verteilt sein, deshalbTextDecoderStreamoderTextDecodermit{ stream: true }verwenden. - Fehler in
start/pullodercontroller.error()versetzen den Stream in den Fehlerzustand. Ab dann schlagen alle weiterenread()-Aufrufe fehl. - In Node.js gibt es neben den Web Streams (
node:stream/web, ab v18 global) die älteren Node-Streams. Umwandeln kannst du mitReadable.fromWeb()undReadable.toWeb().