Back

Daten mit Fetch an den Browser streamen

Daten mit Fetch an den Browser streamen

Die meisten Fetch-API-Tutorials zeigen Ihnen das gleiche Muster: fetch() aufrufen, auf die Antwort warten, .json() oder .text() aufrufen, fertig. Das funktioniert gut für kleine Datenmengen. Aber wenn Ihr Server Daten progressiv generiert – denken Sie an KI-Antworten, Live-Logs oder große Datensätze – ist das Warten auf die vollständige Antwort, bevor Sie ein einziges Byte verarbeiten können, ein echtes Problem.

Die gute Nachricht: Die Fetch-API unterstützt bereits inkrementelles Daten-Streaming im Browser. So verwenden Sie es.

Wichtigste Erkenntnisse

  • Das response.body der Fetch-API stellt einen ReadableStream bereit, mit dem Sie Daten Stück für Stück verarbeiten können, sobald sie eintreffen, anstatt auf die vollständige Payload zu warten.
  • Verwenden Sie response.body.getReader() mit einem TextDecoder für die breiteste Browser-Kompatibilität beim Lesen gestreamter Antworten.
  • Netzwerk-Chunks respektieren keine Nachrichtengrenzen – Sie müssen unvollständige Zeilen selbst puffern und aufteilen, wenn Sie strukturierte Formate wie zeilenweise getrennte JSON-Daten parsen.
  • Kombinieren Sie lang laufende Streams immer mit einem AbortController, damit Sie Anfragen sauber abbrechen können, wenn Benutzer wegnavigieren.

Warum Streaming-Antworten mit der Fetch-API wichtig sind

Wenn Sie response.json() oder response.text() aufrufen, muss der Browser den gesamten Antworttext empfangen, bevor das Promise aufgelöst wird. Bei einer 50-MB-Logdatei oder einem langsamen KI-Completion-Endpunkt bedeutet dies, dass Ihre Anwendung keine Teile der Antwort verarbeiten oder rendern kann, bis das letzte Byte angekommen ist.

Streaming ermöglicht es Ihnen, Daten zu verarbeiten, sobald sie eintreffen – Sie zeigen den ersten Chunk den Benutzern an, während der Rest noch übertragen wird. Das ist eine bedeutende Verbesserung der wahrgenommenen Performance.

Wie die ReadableStream Fetch-API funktioniert

Jede fetch()-Antwort stellt einen ReadableStream auf response.body bereit. Anstatt auf die vollständige Payload zu warten, hängen Sie einen Reader an und ziehen Chunks, sobald sie aus dem Netzwerk kommen.

Der am breitesten kompatible Ansatz ist response.body.getReader():

const response = await fetch('/api/stream')

if (!response.ok) {
  throw new Error(`HTTP error: ${response.status}`)
}

const reader = response.body.getReader()
const decoder = new TextDecoder()

while (true) {
  const { value, done } = await reader.read()
  if (done) break
  console.log(decoder.decode(value, { stream: true }))
}

Jeder value ist ein Uint8Array aus rohen Bytes. TextDecoder konvertiert diese Bytes in einen String. Übergeben Sie { stream: true }, damit der Decoder Mehrbyte-Zeichen korrekt verarbeitet, die möglicherweise über Chunk-Grenzen hinweg aufgeteilt sind.

Hinweis zur asynchronen Iteration: Möglicherweise haben Sie for await (const chunk of response.body) gesehen. Diese Syntax ist sauberer, wird aber in Safari ab Version 18.x nicht unterstützt, daher ist die obige getReader()-Schleife die sicherere Wahl für den Produktivbetrieb. Siehe aktuelle Browser-Unterstützung unter https://caniuse.com/wf-async-iterable-streams.

Text-Streams mit TextDecoderStream dekodieren

Wenn Sie einen Pipeline-Ansatz bevorzugen, übernimmt TextDecoderStream die Dekodierung automatisch:

const response = await fetch('/api/stream')
const reader = response.body
  .pipeThrough(new TextDecoderStream())
  .getReader()

while (true) {
  const { value, done } = await reader.read()
  if (done) break
  console.log(value) // bereits ein String
}

Dies ist übersichtlicher, wenn Sie mehrere Transformationsschritte verketten.

Praktische Überlegungen zum Browser-Streaming mit Fetch

Chunk-Grenzen sind willkürlich. Netzwerk-Chunks richten sich nicht nach Zeilen oder Nachrichten aus. Wenn Sie zeilenweise getrennte JSON-Daten oder SSE-Events parsen, müssen Sie unvollständige Zeilen selbst puffern und bei \n aufteilen.

Streams können nur einmal konsumiert werden. Das Anhängen eines Readers mit getReader() sperrt den Stream für diesen Reader, und sobald Daten gelesen wurden, wird der Body als „disturbed” markiert und kann nicht erneut konsumiert werden. Wenn Sie den Body an zwei Stellen benötigen, rufen Sie response.clone() vor dem Lesen auf:

const response = await fetch('/api/data')
const clone = response.clone()

// Original als Stream lesen
const reader = response.body.getReader()

// Klon anderswo normal verwenden
const text = await clone.text()

Streams mit AbortController abbrechen. Lang laufende Streams sollten abbrechbar sein – besonders wenn Benutzer wegnavigieren:

const controller = new AbortController()

const response = await fetch('/api/stream', {
  signal: controller.signal
})

// Bei Bedarf abbrechen
controller.abort()

Dies verhindert, dass der Browser weiterhin Daten empfängt, die niemand liest.

Fazit

Browser-Streaming mit Fetch ist heute gut unterstützt und praktikabel. Das Kernmuster ist unkompliziert: Holen Sie sich einen Reader von response.body, schleifen Sie mit reader.read(), dekodieren Sie Bytes mit TextDecoder und behandeln Sie Chunk-Grenzen in Ihrem eigenen Puffer. Fügen Sie einen AbortController für die Bereinigung hinzu und beachten Sie, dass Response-Bodies nur einmal konsumiert werden können, wenn Sie die Daten an mehreren Stellen benötigen. Das ist alles, was Sie brauchen, um responsive, inkrementelle Datenerlebnisse im Browser zu erstellen.

Häufig gestellte Fragen (FAQs)

Fetch-Streaming funktioniert mit jeder HTTP-Methode, die einen Response-Body zurückgibt, einschließlich POST, PUT und PATCH. Der ReadableStream auf response.body verhält sich unabhängig von der verwendeten Methode identisch. Der Server muss lediglich eine Chunked- oder Streaming-Antwort senden, damit inkrementelles Lesen sinnvoll ist.

Sie müssen einen String-Puffer pflegen. Hängen Sie jeden dekodierten Chunk an den Puffer an und teilen Sie dann bei Zeilenumbruchzeichen auf. Verarbeiten Sie jede vollständige Zeile als JSON und behalten Sie das abschließende unvollständige Segment im Puffer für den nächsten Chunk. Dies berücksichtigt die Tatsache, dass Netzwerk-Chunks ein JSON-Objekt über zwei Lesevorgänge hinweg aufteilen können.

Ja. Sie können einen SSE-Endpunkt über Fetch-Streaming konsumieren, indem Sie das text/event-stream-Format manuell aus den Chunks parsen. Dies gibt Ihnen mehr Kontrolle über Header, Authentifizierung und Request-Methoden im Vergleich zur EventSource-API, die nur GET-Anfragen unterstützt und begrenzte Header-Anpassungsmöglichkeiten bietet.

Wenn die Verbindung abbricht oder der Stream einen Fehler verursacht, wird das von reader.read() zurückgegebene Promise abgelehnt. Umschließen Sie Ihre Leseschleife mit einem try-catch-Block, damit Ihre Anwendung den Fehler elegant behandeln, den Benutzer benachrichtigen oder die Anfrage bei Bedarf wiederholen kann.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay