Streaming AI Output from Scratch with Vue 3 and DeepSeek
At a time when AI applications are everywhere, many frontend developers run into a core pain point: when calling a large model API, you have to wait for the entire content to be generated before displaying it all at once. In complex questions and long-text reasoning scenarios, users wait for seconds with no feedback on the page, easily creating an illusion of freezing or crashing, which seriously degrades the product experience.
Yet the AI products we use daily, like ChatGPT and Doubao, all share a standout experience highlight — streaming output, character by character, where content appears in real time like a typewriter, completely eliminating blank waiting.
In this article, I will combine Vue3 + Vite + the DeepSeek large model API and walk you through implementing a standard AI streaming conversation feature from four dimensions: underlying principles, core Vue knowledge points, complete hands-on code, and pitfall solutions. Along the way, you will master core Vue exam topics like componentization, reactivity, and two-way binding, allowing even beginners to replicate it with one click.
1. First, Understand the Principle: What Is AI Streaming Output? Why Must You Use It?
1.1 The Drawbacks of Traditional One-Shot Output
Conventional large model API requests use a full-response mode: after the client sends a request, it must wait for the large model to complete all token inference, concatenation, and response. Only after the server returns the complete JSON data does the frontend render the content.
The inference time of a large model depends primarily on question complexity, text length, and transformer computation. In long-text and deep reasoning scenarios, the waiting time multiplies, and the page remains in a blank loading state for a long time, resulting in a terrible user experience.
1.2 The Core Principle of Streaming Output
Streaming output is the core optimization scheme for AI interaction, and its essence is chunked transfer based on a persistent HTTP connection:
After the client establishes a connection with the large model server, the server does not need to wait for all content to be generated. Every time a token (the smallest text unit) is generated, it is pushed to the frontend in real time. The frontend continuously receives and concatenates the text, achieving the visual effect of character-by-character printing.
A simple analogy: one-shot output is like "filling a bucket of water and then pouring it out"; streaming output is like "turning on the tap, with water flowing out continuously."
1.3 Core Frontend-Backend Conventions (Must Remember)
- Server-side convention: The API accepts a
stream: trueparameter to enable streaming transfer mode and return fragmented tokens in real time. - Client-side convention: The request parameters carry
stream: true, and the browser'sReadableStreamis used to read the binary stream, continuously parsing and rendering.
This is also the core watershed for AI product user experience, and an essential core knowledge point for frontend AI development.
2. Preliminaries: Core Vue3 Knowledge Points for This Hands-On
This hands-on is based on Vue3's script setup syntax and uses modern Vue development ideas throughout. Let's quickly sort out the core knowledge points first; understanding them before coding yields twice the result with half the effort.
2.1 Component-Based Thinking: The Core of Frontend Engineering
The core idea of modern frameworks like Vue and React is componentization: pages are no longer composed of scattered HTML, CSS, and JS tags, but are assembled from reusable, maintainable, independently encapsulated components (.vue files).
Each .vue component is uniformly divided into three parts, each with its own responsibility:
- template: Replaces static HTML, supports data binding and reactive updates, and serves as the structural carrier of the page.
- script: Handles business logic, requests, and data state, implementing page interaction capabilities.
- style: Responsible for page aesthetics, supporting scoped style isolation.
2.2 Data Binding and Reactivity: Say Goodbye to Native DOM Operations
Vue's core advantage is data-driven views, completely abandoning the tedious native JS operations of DOM retrieval, assignment, and rendering.
- One-way binding ({{}}): When data changes, the view updates automatically. Suitable for pure display scenarios.
- Two-way binding (v-model): Data changes update the view, and view modifications (user input) synchronously update the data. Specifically adapted for form input scenarios.
- ref reactive data: Vue3's core API for defining reactive state for primitive types. Data changes trigger view updates in real time, perfectly suited for the dynamic content rendering scenario of AI conversations.
2.3 Vite Environment Variable Configuration
Vite projects can configure private environment variables via .env.local to securely store sensitive information like API keys. They are read globally via import.meta.env, avoiding the leakage of hardcoded keys.
3. Step-by-Step Hands-On: Complete Implementation of Vue3 + DeepSeek Streaming Conversation
This hands-on implements complete functionality: custom questions, streaming/non-streaming output toggle, real-time AI replies, and status prompts. The code can be copied and run directly.
3.1 Environment Setup
- Scaffold a Vite + Vue3 project:
npm create vite@latest ai-stream-demo -- --template vue - Install dependencies:
npm install - Create
.env.localin the project root and configure the DeepSeek key:VITE_DEEPSEEK_API_KEY=your DeepSeek key
3.2 Complete Business Code (Directly Reusable)
Replace all the code in the project's App.vue, including template, logic, and styles. The functionality is complete without any cuts:
<template>
<div class="container">
<!-- Question input area -->
<div class="input-box">
<label>Enter your question:</label>
<input v-model="question" placeholder="Please enter your question" @key.enter="update">
<button @click="update" class="submit-btn">Submit Question</button>
</div>
<!-- Stream toggle + AI reply area -->
<div class="output-box">
<div class="stream-switch">
<label>Enable streaming output:</label>
<input type="checkbox" v-model="stream" />
</div>
<p class="content-text">{{ content }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// Reactive state definitions
const question = ref('Tell a story about a Chinese dragon')
const stream = ref(false)
const content = ref('')
// Core: Interfacing with the DeepSeek streaming API
const update = async () => {
if (!question.value.trim()) return alert('Please enter a valid question!')
content.value = 'AI is thinking...'
// API endpoint + request header configuration
const endpoint = 'https://api.deepseek.com/chat/completions'
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`
}
try {
const response = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify({
model: 'deepseek-v4-flash',
messages: [{ role: 'user', content: question.value }],
stream: stream.value
})
})
// Core logic for streaming output
if (stream.value) {
content.value = ''
const reader = response.body?.getReader()
const decoder = new TextDecoder('UTF-8')
let done = false, buffer = ''
// Loop to read the binary stream, parsing and rendering character by character
while (!done) {
const { value, done: doneReading } = await reader.read()
done = doneReading
if (value) {
buffer += decoder.decode(value, { stream: true })
// Filter valid streaming data and concatenate text
const validLines = buffer.split('\n').filter(line => line.startsWith('data: '))
for (const line of validLines) {
const jsonStr = line.replace('data: ', '').trim()
if (jsonStr === '[DONE]') return
const resData = JSON.parse(jsonStr)
content.value += resData.choices[0].delta?.content || ''
}
}
}
} else {
// Non-streaming one-shot return
const data = await response.json()
content.value = data.choices[0].message.content
}
} catch (error) {
content.value = 'Request failed, please check your key or network configuration!'
console.error('AI request error:', error)
}
}
</script>
<style scoped>
.container { width: 800px; margin: 50px auto; padding: 20px; background: #f5f7fa; border-radius: 12px; }
.input-box { margin-bottom: 20px; display: flex; align-items: center; gap: 10px; }
.input { flex: 1; padding: 10px 15px; border: 1px solid #e5e6eb; border-radius: 6px; font-size: 14px; }
.submit-btn { padding: 10px 20px; background: #1677ff; color: #fff; border: none; border-radius: 6px; cursor: pointer; }
.stream-switch { margin-bottom: 15px; }
.content-text { padding: 15px; background: #fff; border-radius: 8px; min-height: 60px; line-height: 1.8; white-space: pre-wrap; }
</style>
4. In-Depth Core Code Analysis (Common Interview Questions)
4.1 Core APIs for Streaming Output
- response.body: Returns the browser's native
ReadableStreamreadable stream object, the core carrier for streaming transfer. - getReader(): Creates a stream reader that exclusively locks the response stream, receiving binary data sent back by the server chunk by chunk.
- TextDecoder: A decoder that converts binary data to UTF-8 text, solving the problem of garbled streaming data.
4.2 DeepSeek Streaming Data Format Analysis
DeepSeek's streaming response is a line-by-line string in the standard format: data: {"id":"xxx","choices":[{"delta":{"content":"text content"}}]}
A fixed identifier data: [DONE] is returned at the end, used to determine whether the streaming transfer is complete and to terminate the read loop.
4.3 The Role of the Buffer Cache
The tokens returned by the large model stream are fragmented, and a single read may contain incomplete data. Using a buffer to cache and concatenate effectively avoids text loss and content disorder, ensuring the completeness of the reply.
5. Common Pitfalls and Solutions
- Key invalid / 401 request: Check the
.env.localfile name, ensure the variable prefix isVITE_, restart the project for changes to take effect, and make sure the key has no extra spaces. - Garbled streaming output: Specify the encoding as UTF-8 for TextDecoder; do not use the default encoding.
- Text duplication / loss: You must use a buffer to cache fragmented data; do not directly render the result of a single read.
- Cross-origin issues: For local debugging, you can enable cross-origin mode in the browser. For production, the backend needs to configure a proxy to forward API requests.
6. Summary and Expansion
In this article, we implemented a Vue3 + DeepSeek streaming AI conversation from scratch. We not only nailed the core interactive experience of enterprise-grade AI products but also thoroughly mastered Vue3 reactivity, componentization, two-way binding, and the underlying principles of frontend streaming transfer.
Streaming output is not a simple API call; it is a comprehensive capability involving frontend user experience optimization, network transmission, and data parsing. It is also a high-frequency interview topic for current AI frontend positions.
Expansion directions: Based on this case, you can continue iterating to implement features like conversation history, loading animations, error retries, multi-turn conversations, and markdown syntax rendering, quickly building a complete AI chat project.