ChatGPT-Antworten live streamen im Browser (OpenAI, fetch + SSE)
Statt auf die komplette KI-Antwort zu warten, lässt du sie Wort für Wort erscheinen – wie im echten ChatGPT. So liest du den Server-Sent-Events-Stream der OpenAI-API mit reinem fetch, ohne Bibliothek.
Nichts wirkt bei einem KI-Feature träger, als sekundenlang auf die fertige Antwort zu warten. Die Lösung ist Streaming: Die OpenAI-API schickt die Antwort als Server-Sent Events (SSE) häppchenweise, und du zeigst jedes Stück sofort an – genau wie im echten ChatGPT. Mit fetch und dem ReadableStream brauchst du dafür keine Bibliothek.
Den Stream Token für Token lesen
async function streamChat(messages, onToken) {
const res = await fetch('/api/chat', { // DEIN Backend, nicht direkt OpenAI!
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages }),
})
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { value, done } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
// SSE trennt Events durch Doppel-Zeilenumbruch
const parts = buffer.split('\n\n')
buffer = parts.pop() // Rest bis zum nächsten Chunk aufheben
for (const part of parts) {
const line = part.trim()
if (!line.startsWith('data:')) continue
const data = line.slice(5).trim()
if (data === '[DONE]') return // Ende-Signal von OpenAI
const json = JSON.parse(data)
const token = json.choices?.[0]?.delta?.content
if (token) onToken(token) // Stückchen sofort anzeigen
}
}
}
// Nutzung: Text wächst live im DOM
const out = document.querySelector('#answer')
streamChat(
[{ role: 'user', content: 'Erklär mir Streaming in einem Satz.' }],
(t) => { out.textContent += t }
)Dein Backend (/api/chat) ruft OpenAI mit stream: true auf und leitet den Stream 1:1 weiter:
// Node/Server-Seite – hier lebt der API-Key
const upstream = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ model: 'gpt-4o', stream: true, messages }),
})
// upstream.body direkt an den Client durchreichenZwei Dinge, die oft schiefgehen
- Chunks sind keine ganzen Events. Ein
fetch-Chunk kann mitten in einer Zeile enden. Deshalb derbuffer: Erst am\n\nsplitten, den unvollständigen Rest aufheben. Wer jeden Chunk direkt als JSON parst, bekommt sporadisch Fehler. - Der API-Key gehört auf den Server. Rufe nie
api.openai.comdirekt aus dem Browser auf – der Key wäre sofort öffentlich. Der Client spricht immer mit deinem eigenen Endpunkt, der den Key hält und den Stream weiterreicht.
Das gleiche Muster funktioniert übrigens auch mit Claude (Anthropic) und Gemini – alle drei liefern Streaming über denselben SSE-Mechanismus, nur mit leicht anderem JSON.
Du willst so ein flüssiges KI-Erlebnis auf deiner Seite? Ich helfe dir weiter.
Quellen
Du brauchst mehr als ein Snippet?
Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.
Projekt anfragen →Verwandte Snippets
Web Worker – rechnen, ohne die Oberfläche einzufrieren
JavaScript hat im Browser genau einen Faden für alles: Klicks, Animationen, Layout – und deine Rechenschleife. Dauert die zwei Sekunden, ist die Seite zwei Sekunden lang tot. Ein Worker nimmt die Arbeit auf einen eigenen Faden.
Formulare ohne Neuladen abschicken – FormData und fetch
Ein Formular per JavaScript abzuschicken heißt nicht, jedes Feld einzeln auszulesen. FormData nimmt das ganze Formular, fetch schickt es weg – inklusive Dateien, und mit einer Fehlermeldung, die der Besucher versteht.
CustomEvent – Bausteine, die sich gegenseitig Bescheid sagen, ohne sich zu kennen
Sobald zwei Teile einer Seite aufeinander reagieren sollen, landet man schnell bei globalen Variablen oder Funktionsaufrufen quer durchs Projekt. Eigene Events entkoppeln das – mit denselben Mitteln, die der Browser selbst benutzt.