Start · Sprachen · JavaScript · Referenz · ReadableStream.tee()

ReadableStream.tee()

Methode

Teilt einen <code>ReadableStream</code> in zwei unabhängige Zweige auf und gibt sie als Array <code>[branch1, branch2]</code> zurück.

seit JavaScript Baseline: alle modernen Browser, Node.js 16.5+ (gl Kategorie: webapi

Signatur

ReadableStream.prototype.tee()

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

Typ
[ReadableStream, ReadableStream]
Beschreibung
Ein Array mit genau zwei neuen 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);
true abc abc

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');
12 % 47 % 100 % Fertig: 1048576 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);
1 2 3

// 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 von getReader() oder tee() auf dem Original einen TypeError. Ist der Stream bereits gesperrt (z. B. durch einen aktiven Reader), wirft tee() ebenfalls einen TypeError.
  • 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/web bzw. 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 ist response.clone() meist bequemer, da Header und Status erhalten bleiben.