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
toSorted, toReversed und with: Arrays sortieren, ohne das Original zu zerstören
array.sort() sortiert nicht eine Kopie, sondern das Array selbst – und genau daran ist schon mancher Zustand im Frontend stillschweigend kaputtgegangen. Seit ES2023 gibt es zu jeder dieser Methoden eine Variante, die eine Kopie zurückgibt.
Aufzählungen richtig verketten mit Intl.ListFormat
"a, b und c" von Hand zusammenzubauen geht fast immer schief – bei einem Element, bei zwei Elementen, beim letzten Komma. Intl.ListFormat macht es richtig, in jeder Sprache, ganz ohne Bibliothek.
requestAnimationFrame – die Animationsschleife, die zum Bildschirm passt
setInterval(schritt, 16) läuft auf einem 144-Hz-Monitor anders als auf einem alten Handy – und im Hintergrundtab einfach weiter. requestAnimationFrame löst beides, wenn man die verstrichene Zeit mitrechnet statt Bilder zu zählen.