How to Build a Typewriter Effect for LLM Streaming with Vue 3 and ReadableStream
📝Abstract
In AI applications and intelligent Agent development, streaming output has become the standard interaction for conversational interfaces. Instead of waiting for the large model to generate the entire response before rendering it all at once, you can use the browser's native
ReadableStreamwith the DeepSeek large model API to easily create a typewriter animation that displays content as it is generated. This article explains the underlying principles and engineering approach layer by layer, with practical Vue3<script setup>code. You don't need to copy the entire code; once you grasp the core logic, you can migrate it to any frontend project.
🤖 1. In the Age of Agents, How Should We Adjust Our Development Mindset?
Today's large model Agents are continuously iterating, with capabilities increasingly approaching the autonomous completion of complex tasks, marking an important direction towards AGI. A new model of human-machine collaboration is taking shape: delegate the standardized, repetitive work that AI excels at to Agents, while humans focus on decision-making, review, and core business control.
When it comes to frontend development, there are two highly practical approaches:
- Refuse to build project scaffolding from scratch Don't manually write
index.html,App.vue, and other basic files every time you start a new project. Instead, go directly to Github to pull a mature template and iterate on your business logic on top of it. - Reasonably divide the work boundaries between humans and AI Hand off project initialization, boilerplate code, and common base component construction to the Agent; developers should lead and control complex interaction logic, edge-case exception handling, and core business rules.
⚡ 2. Vite Hot Reload: An Efficiency Booster for Debugging Streaming Projects
This demo is developed based on Vite + Vue3. Vite's built-in Hot Reload is a powerful tool for local development.
- Traditional bundling mode: Modify code → Full page refresh → All page runtime state is lost. Imagine debugging streaming conversations: every time you change the code, you have to re-enter the prompt, wasting a lot of time on ineffective operations.
- Hot Reload mechanism: Only the modified module is updated, preserving the page's current data and runtime state, achieving a partial refresh. This saves a lot of repetitive work when debugging LLM streaming logic.
🌊 3. The Underlying Principle of Streaming Output: Binary ReadableStream
Traditional One-Shot Request Mode
Frontend initiates a request → The large model server computes the entire answer → Returns a complete JSON → The page renders it all at once. When the AI outputs long text, the user sees nothing but "Loading..." for a long time, resulting in a poor interactive experience.
Stream Mode
The large model continuously sends incremental text chunks to the frontend as it generates tokens. The frontend receives the chunked data and immediately appends it for rendering, which is the familiar typewriter effect.
At the browser level, the streaming response returned by the LLM is essentially a binary data stream, ReadableStream, which can be understood using a water pipe analogy:
response.body: The pipe (ReadableStream) returned by the server;.getReader(): Obtains a reader to continuously pull data from the pipe;reader.read(): Each call pulls out a chunk ofUint8Arraybinary data (unsigned integers ranging from 0 to 255);TextDecoder: Binary bytes cannot be directly converted into readable text; a decoder is needed to perform the translation.
📡 4. LLM Streaming Communication Specification: SSE-like Chunk Format
Mainstream large model streaming interfaces, such as those from DeepSeek and OpenAI, uniformly follow data chunking rules similar to SSE:
- Each valid data line starts with
data:; - When all content has been pushed, the server sends
data: [DONE]as the termination marker.
Example raw stream snippet:
plaintext
data: {"choices":[{"delta":{"content":"Once"}}]}
data: {"choices":[{"delta":{"content":" upon"}}]}
data: [DONE]
Standard frontend processing flow:
- Split the decoded string by newline characters;
- Filter and select the data lines starting with
data:; - Filter out the termination marker
[DONE]; - Parse the chunk as JSON and extract the
delta.contentincremental text; - Continuously concatenate the text and update the page view.
⚠️ Two common pitfalls for beginners:
- The native JS string method is
startsWith. It's very easy to mistakenly writestartWith, which throws a "function does not exist" error;- A single read of a binary chunk might truncate a JSON string. You must set up a
bufferto save the incomplete fragment for parsing in the next loop iteration.
💻 5. Core Vue3 Practical Approach (Key Point! Don't Just Copy the Code Blindly)
Using Vue3's <script setup> Composition API offers clear advantages over Vue2's Options API: logic related to the same business concern is grouped together, making the code more cohesive and easier to maintain.
Basic Reactive Variables
js
import { ref } from 'vue';
// User's question
const question = ref('Tell a story about a Chinese dragon');
// AI output content
const content = ref('');
// Toggle: whether to enable streaming output
const stream = ref(true);
Core Request Logic
js
const update = async () => {
if (!question.value) return;
content.value = 'Thinking...';
const endpoint = 'https://api.deepseek.com/chat/completions';
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`
},
body: JSON.stringify({
model: 'deepseek-v4-flash',
messages: [{ role: 'user', content: question.value }],
stream: stream.value
})
});
if(stream.value){
// ========== 🎯 Core Streaming Logic ==========
content.value = '';
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let done = false;
let buffer = ''; // Cache for incomplete JSON strings
while (!done) {
const { value, done: doneReading } = await reader.read();
done = doneReading;
if(!value) continue;
// Decode binary and concatenate with buffer
const chunk = buffer + decoder.decode(value);
buffer = '';
const lines = chunk.split('\n');
for(const line of lines){
const trimLine = line.trim();
if(!trimLine.startsWith('data: ')) continue;
const dataStr = trimLine.replace('data: ','');
if(dataStr === '[DONE]') continue;
try{
const res = JSON.parse(dataStr);
const text = res.choices[0].delta.content;
if(text) content.value += text;
}catch{
// JSON is incomplete, store in buffer for next round of parsing
buffer = line;
}
}
}
}else{
// Non-streaming: receive the complete result at once
const data = await response.json();
content.value = data.choices[0].message.content;
}
}
✅ Three core blocks you need to master; the rest of the code can be trimmed or modified as needed:
- The
whileloop continuously callsreader.read()to pull binary chunks; - The
bufferfor fault tolerance, solving the problem of truncated JSON; - Continuously appending incremental text, leveraging Vue's reactivity to automatically update the page.
Minimal template reference:
vue
<template>
<div class="container">
<div>
<label>Input:</label>
<input v-model="question"/>
<button @click="update">Submit</button>
</div>
<div>
<label>Streaming</label>
<input type="checkbox" v-model="stream"/>
</div>
<div>{{ content }}</div>
</div>
</template>
🚨 6. Three Risks You Must Address Before Going Live
- CORS Cross-Origin Restrictions Calling the DeepSeek public API directly from the browser frontend will trigger a cross-origin error. For local development, you can use a Vite proxy; in a production environment, you must add a backend relay layer.
- API Key Leakage Keys inside a frontend project cannot be kept secret; a user can simply open the browser's developer tools to grab them. Online projects must not make direct frontend requests to large model APIs.
- Network Exception Handling You need fallback handling for disconnections, timeouts, and chunk parsing failures. The
buffercannot be casually removed; it is key to the stable operation of streaming.
🧠 7. Expansion: The Value of Streaming Output in Agent Development
Streaming output is not just about implementing a nice typewriter animation. In intelligent Agent scenarios, the AI completes thinking, tool calling, and result summarization step by step. Relying on streaming push, we can display the Agent's complete chain of thought and tool call logs in real time, allowing users to clearly see how the AI breaks down tasks and executes actions.
Mastering browser data stream processing is a fundamental skill for developing AI conversation and intelligent Agent frontend interfaces.