跪拜 Guibai
← All articles
Frontend · Vue.js

Streaming AI Output from Scratch with Vue 3 and DeepSeek

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

Streaming is the dividing line between a clunky AI UI and a polished product. Knowing how to consume a raw byte stream, parse server-sent tokens, and handle partial data is now a baseline skill for any frontend developer building on LLM APIs.

Summary

The core problem is the standard full-response API pattern: a frontend sends a request, waits seconds for the entire LLM output, and only then renders anything. Streaming output fixes this by keeping an HTTP connection open and pushing each generated token to the client immediately. The browser's ReadableStream API, a TextDecoder, and a buffer to reassemble fragmented chunks turn that stream into a smooth character-by-character display.

A full Vue 3 + Vite implementation is provided, covering environment setup, API key storage via .env.local, and a single-file App.vue component with reactive state, two-way binding on the question input, and a checkbox toggle between streaming and non-streaming modes. The streaming path uses response.body.getReader() to loop over binary chunks, parses DeepSeek's line-delimited JSON format, and stops when the [DONE] sentinel arrives.

Common failure points are addressed directly: 401 errors from misconfigured environment variables, garbled text from wrong decoder encoding, lost content when skipping the buffer, and local CORS workarounds. The piece closes with expansion ideas like conversation history, markdown rendering, and error retries.

Takeaways
Streaming output keeps an HTTP connection open and pushes each token to the client as it is generated, rather than waiting for the full response.
The browser's ReadableStream and getReader() provide the low-level primitives for consuming a chunked response.
A TextDecoder with explicit UTF-8 encoding prevents garbled characters from binary stream data.
A buffer variable is necessary to accumulate partial chunks; rendering each read directly causes text loss or duplication.
DeepSeek's streaming format is line-delimited JSON prefixed with 'data: ' and terminated by 'data: [DONE]'.
Vite environment variables must use the VITE_ prefix and be stored in .env.local to avoid leaking API keys into source control.
Vue 3 refs drive the typewriter effect: appending each parsed token to a reactive string re-renders the UI automatically.
Conclusions

The streaming implementation is surprisingly small — about 20 lines of JavaScript — because the browser platform already ships the hard parts (ReadableStream, TextDecoder). Most of the work is understanding the data format and handling edge cases.

Toggling between streaming and non-streaming modes in the same component is a sharp teaching device; it makes the latency difference visceral and proves the streaming path works by direct comparison.

The article treats the buffer as a first-class concern, which is correct: real-world streaming code that skips buffering will fail intermittently and be hard to debug, because chunk boundaries do not align with message boundaries.

Concepts & terms
ReadableStream
A browser API representing a stream of binary data that can be read incrementally. The getReader() method returns a reader that locks the stream and provides read() calls returning chunks as they arrive.
TextDecoder
A built-in JavaScript object that converts a stream of bytes into a string using a specified character encoding (e.g., UTF-8). The { stream: true } option tells it to handle multi-byte characters split across chunks.
Server-Sent Events (SSE) vs. Streaming Fetch
SSE is a standardized protocol where the server pushes 'data:' prefixed lines over a long-lived HTTP connection. The approach here uses a raw fetch with a ReadableStream, which gives more control but requires manual parsing of the same line-delimited format.
DeepSeek Chat Completions API
An OpenAI-compatible endpoint at api.deepseek.com/chat/completions. Setting stream: true in the request body causes the server to return tokens as they are generated rather than in a single JSON response.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗