Signatur
Beschreibung
tee() teilt einen lesbaren Stream wie ein T-Stück in zwei Zweige auf. Beide Zweige erhalten dieselben Chunks in derselben Reihenfolge und lassen sich unabhängig voneinander lesen, weiterleiten oder abbrechen. Der ursprüngliche Stream wird dabei gesperrt (locked === true) und kann danach nicht mehr direkt gelesen werden.
Typische Einsatzfälle sind Situationen, in denen ein Stream nur einmal konsumiert werden kann, die Daten aber zweimal gebraucht werden: etwa eine fetch-Antwort gleichzeitig anzeigen und im Cache ablegen, einen Upload gleichzeitig hashen und speichern oder Fortschritt messen, während die Daten weiterverarbeitet werden. Genau so arbeitet intern auch Response.prototype.clone().
Wichtig: Die Zweige teilen sich eine gemeinsame Quelle. Liest ein Zweig schneller als der andere, werden die Chunks für den langsameren Zweig unbegrenzt zwischengepuffert – es gibt keinen Gegendruck (Backpressure) vom langsamsten Zweig. Die Quelle wird erst dann abgebrochen, wenn beide Zweige abgebrochen wurden.
Bei gewöhnlichen Streams werden die Chunks nicht kopiert: Beide Zweige erhalten dieselbe Objektreferenz. Wer einen Chunk in einem Zweig verändert, verändert ihn auch im anderen. Bei Byte-Streams (type: "bytes") werden die Daten dagegen für den zweiten Zweig kopiert.
Rückgabewert
ReadableStream-Instanzen, die beide dieselben Chunks des Ursprungs-Streams liefern.Beispiele
Grundprinzip: zwei Zweige lesen dieselben Daten
const stream = new ReadableStream({
start(controller) {
['a', 'b', 'c'].forEach((chunk) => controller.enqueue(chunk));
controller.close();
}
});
const [links, rechts] = stream.tee();
console.log(stream.locked); // Ursprung ist nun gesperrt
const sammeln = async (s) => {
const teile = [];
for await (const chunk of s) teile.push(chunk);
return teile.join('');
};
const [a, b] = await Promise.all([sammeln(links), sammeln(rechts)]);
console.log(a, b);
fetch-Antwort anzeigen und gleichzeitig cachen
const ladeUndCache = async (url) => {
const response = await fetch(url);
const [fuerCache, fuerAnzeige] = response.body.tee();
// Zweig 1: im Cache Storage ablegen
const cache = await caches.open('v1');
const cachePromise = cache.put(url, new Response(fuerCache, { headers: response.headers }));
// Zweig 2: als Text lesen und anzeigen
const text = await new Response(fuerAnzeige).text();
await cachePromise;
return text;
};
const html = await ladeUndCache('/artikel.html');
console.log(html.length);
Download-Fortschritt messen, während die Daten verarbeitet werden
const response = await fetch('/grosse-datei.bin');
const gesamt = Number(response.headers.get('Content-Length')) || 0;
const [zaehlZweig, datenZweig] = response.body.tee();
const fortschritt = (async () => {
let geladen = 0;
for await (const chunk of zaehlZweig) {
geladen += chunk.byteLength;
if (gesamt) console.log(`${Math.round((geladen / gesamt) * 100)} %`);
}
})();
const blob = await new Response(datenZweig).blob();
await fortschritt;
console.log('Fertig:', blob.size, 'Bytes');
Einen Zweig abbrechen, den anderen weiterlesen
const stream = new ReadableStream({
start(c) { [1, 2, 3].forEach((n) => c.enqueue(n)); c.close(); },
cancel(grund) { console.log('Quelle abgebrochen:', grund); }
});
const [a, b] = stream.tee();
await a.cancel('nicht benötigt'); // Quelle läuft weiter, da b noch aktiv ist
for await (const n of b) console.log(n);
// Wichtig · Fallstricke
- Kein Gegendruck vom langsamen Zweig: Wird ein Zweig nie oder nur langsam gelesen, puffert der Browser alle Chunks dafür im Speicher. Bei großen Downloads kann das zu hohem Speicherverbrauch führen – ungenutzte Zweige daher mit
cancel()abbrechen. - Gesperrter Ursprung: Nach
tee()wirft ein erneuter Aufruf vongetReader()odertee()auf dem Original einenTypeError. Ist der Stream bereits gesperrt (z. B. durch einen aktiven Reader), wirfttee()ebenfalls einenTypeError. - Geteilte Chunk-Referenzen: Bei normalen Streams erhalten beide Zweige dasselbe Objekt. Chunks nicht mutieren oder vorher kopieren (z. B.
structuredClone()). - Abbruch: Die zugrunde liegende Quelle wird erst abgebrochen, wenn beide Zweige abgebrochen wurden; der Grund ist dann ein Array beider Abbruchgründe.
- Kompatibilität: In allen aktuellen Browsern verfügbar; in Node.js über
node:stream/webbzw. ab Version 18 global. Das Kopierverhalten für Byte-Streams wurde erst später spezifiziert und kann in älteren Engines abweichen. - Für ganze
Response-Objekte istresponse.clone()meist bequemer, da Header und Status erhalten bleiben.