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
Verlässliches JSON aus einem LLM – mit Structured Outputs statt Hoffnung
Antworte nur mit JSON" reicht nicht – irgendwann kommt doch ein "Klar, hier ist dein JSON:" davor und dein JSON.parse() stürzt ab. Mit einem JSON-Schema erzwingst du garantiert gültige Struktur.
fetch() mit async/await und sauberem try/catch
Daten von einer API holen ist mit fetch() und async/await lesbar wie synchroner Code. Der Haken: fetch wirft bei 404 oder 500 keinen Fehler – du musst response.ok selbst prüfen.
Debounce in Vanilla JavaScript – ohne Bibliothek
Eine Suche, die bei jedem Tastendruck feuert, überlastet den Server. Mit einer kleinen debounce-Funktion wartest du, bis der Nutzer kurz Pause macht – in wenigen Zeilen, ganz ohne Framework.