How to Build a Typewriter Effect for LLM Streaming with Vue 3 and ReadableStream
Streaming turns a slow LLM call into a responsive UI, but the ReadableStream + SSE parsing loop is full of footguns — truncated JSON, missing `startsWith` typos, and key leaks — that every frontend developer building AI features will hit.
Modern AI chat interfaces skip the loading spinner by rendering tokens as they arrive. The browser's native ReadableStream API makes this possible: `response.body.getReader()` pulls binary chunks, a TextDecoder converts them to strings, and the frontend splits the result on newlines to extract SSE-formatted `data:` payloads. A buffer variable is essential because a single read can slice a JSON object in half, breaking `JSON.parse`.
A Vue 3 `<script setup>` implementation against the DeepSeek API shows the full loop: a `while` block calls `reader.read()`, appends decoded text to a buffer, filters lines that start with `data:`, ignores the `[DONE]` sentinel, and concatenates `delta.content` into a reactive string. The same pattern works for OpenAI-compatible endpoints.
Production deployments need a backend proxy. Calling the LLM directly from the browser exposes API keys and hits CORS errors. The buffer logic also doubles as a fault-tolerance mechanism for network hiccups and truncated chunks.
The streaming loop is mechanically simple but fragile: a single typo like `startWith` instead of `startsWith` silently breaks the parser, and omitting the buffer causes intermittent JSON parse failures that are hard to reproduce.
Streaming output is not just cosmetic. In agent UIs, it lets users watch the model's chain-of-thought and tool calls unfold in real time, which is critical for trust and debuggability.
Frontend-only LLM calls are a security non-starter for anything beyond local demos; the article's warning about key leakage is understated given how many hobby projects ship API keys in client bundles.