跪拜 Guibai
← All articles
Frontend · Vue.js

How to Build a Typewriter Effect for LLM Streaming with Vue 3 and ReadableStream

By 只一 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

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.

Summary

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.

Takeaways
LLM streaming responses are binary ReadableStreams consumed via `response.body.getReader()`.
A TextDecoder converts the `Uint8Array` chunks into strings that can be split on newlines.
Each line starting with `data:` carries a JSON payload; `data: [DONE]` signals the end of the stream.
A buffer variable must hold incomplete lines because a single binary chunk can truncate a JSON string mid-object.
Calling the LLM API directly from the browser exposes the API key and triggers CORS errors; a backend proxy is required for production.
Vite's hot reload preserves page state during streaming development, avoiding repeated prompt submissions after each code change.
Conclusions

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.

Concepts & terms
ReadableStream
A browser API representing a stream of binary data from a network response. Its `getReader()` method returns a reader that pulls chunks of `Uint8Array` bytes as they arrive.
SSE (Server-Sent Events) chunk format
A convention used by LLM APIs where each streaming update is a newline-delimited line prefixed with `data:`, carrying a JSON payload. The stream ends with `data: [DONE]`.
Buffer (in streaming context)
A string variable that holds an incomplete line of data when a binary chunk cuts a JSON object in half. The buffer is prepended to the next chunk before parsing, preventing `JSON.parse` failures.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗