前端流式输出完全指南:原理、实现与工程化实践

前言

在 AI 问答、实时日志、消息推送、大文本展示等场景中,传统一次性请求等待时间长、体验差,而流式输出可以实现 "收到一段、渲染一段",显著降低首屏等待时间、提升流畅度。

本文不讲虚、不堆砌概念,用最通俗的语言和可直接运行的代码,把前端流式输出从原理到实践讲透。

一、流式输出核心原理

1.1 什么是流式输出?

流式输出(Streaming)是指服务端不以完整数据包返回,而是将内容拆分成多个小块(Chunk),持续推送到前端;浏览器收到一块就解析一块,实现渐进式渲染

与传统请求的区别:

  • 传统请求:等待全部数据返回 → 一次性渲染
  • 流式输出:边接收、边解析、边渲染

1.2 优势

  • 首屏展示更快,无需等待全量返回
  • 内存占用更低,不缓存超大文本
  • 用户体验更流畅(打字机效果)
  • 适合超长文本、实时日志、AI 回复

1.3 关键技术支撑

  • HTTP/1.1 Chunked Transfer Encoding(分块传输)
  • Fetch + ReadableStream
  • TextDecoder 字符解码
  • SSE(Server-Sent Events)
  • WebSocket(双向实时通信)

二、原生 JavaScript 实现流式输出

2.1 Fetch + ReadableStream 标准实现

这是目前最通用、最适合 AI 打字机的方案。

复制代码
async function readStream(url, callback) {
  const response = await fetch(url)
  const reader = response.body.getReader()
  const decoder = new TextDecoder('utf-8', { stream: true })

  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    // 解码并返回片段
    const chunk = decoder.decode(value)
    callback(chunk)
  }
}

使用示例

复制代码
const output = document.querySelector('#output')

readStream('/api/stream', (text) => {
  output.innerHTML += text
  output.scrollTop = output.scrollHeight
})

关键点说明:

  • response.body.getReader() 获取流读取器
  • TextDecoder 用于将二进制流转为字符串
  • { stream: true } 避免中文截断乱码
  • 循环 read() 直到 done = true

2.2 SSE 方式实现(服务端推送)

适合简单单向推送,使用浏览器原生 EventSource。

复制代码
const eventSource = new EventSource('/api/sse')

eventSource.onmessage = (event) => {
  const text = event.data
  output.innerHTML += text
}

eventSource.onerror = () => {
  eventSource.close()
}

优点:开箱即用、自动重连。缺点:只支持 GET、无法自定义请求头。


三、Vue 3 项目中实现流式输出

以下是生产环境可直接使用的封装。

复制代码
<template>
  <div class="stream-content" ref="contentRef">{{ result }}</div>
</template>

<script setup>
import { ref, onUnmounted } from 'vue'

const result = ref('')
const contentRef = ref(null)
let abortController = null

async function startStream() {
  abortController = new AbortController()
  result.value = ''

  try {
    const response = await fetch('/api/stream', {
      signal: abortController.signal
    })

    const reader = response.body.getReader()
    const decoder = new TextDecoder('utf-8', { stream: true })

    while (true) {
      const { done, value } = await reader.read()
      if (done) break

      result.value += decoder.decode(value)

      // 自动滚动到底部
      nextTick(() => {
        if (contentRef.value) {
          contentRef.value.scrollTop = contentRef.value.scrollHeight
        }
      })
    }
  } catch (err) {
    console.error('流读取异常', err)
  }
}

// 组件卸载时停止流
onUnmounted(() => {
  abortController?.abort()
})

startStream()
</script>

四、React 项目中实现流式输出

复制代码
import { useState, useEffect, useRef } from 'react'

export default function Stream() {
  const [text, setText] = useState('')
  const ref = useRef(null)
  const controllerRef = useRef(null)

  useEffect(() => {
    async function read() {
      const controller = new AbortController()
      controllerRef.current = controller

      const res = await fetch('/api/stream', { signal: controller.signal })
      const reader = res.body.getReader()
      const decoder = new TextDecoder('utf-8', { stream: true })

      while (true) {
        const { done, value } = await reader.read()
        if (done) break

        const chunk = decoder.decode(value)
        setText((prev) => prev + chunk)
        
        if (ref.current) {
          ref.current.scrollTop = ref.current.scrollHeight
        }
      }
    }

    read()

    return () => {
      controllerRef.current?.abort()
    }
  }, [])

  return <div ref={ref} style={{ height: 400, overflow: 'auto' }}>{text}</div>
}

五、性能优化与工程化实践

5.1 避免高频渲染卡顿

流数据推送过快会导致页面卡顿,可使用缓冲合并

复制代码
let buffer = []
let timer = null

function pushChunk(chunk) {
  buffer.push(chunk)
  if (!timer) {
    timer = setTimeout(() => {
      callback(buffer.join(''))
      buffer = []
      timer = null
    }, 30)
  }
}

5.2 XSS 防护

流式内容必须转义,防止恶意脚本执行。

复制代码
function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;')
}

5.3 取消流与防止内存泄漏

使用 AbortController 取消请求,组件卸载时必须中止。

复制代码
const controller = new AbortController()

fetch(url, { signal: controller.signal })

// 取消
controller.abort()

六、常见问题与排查

6.1 流不生效

  • 服务端未返回 Transfer-Encoding: chunked
  • Nginx 未配置 proxy_buffering off;
  • 服务端未保持长连接

6.2 中文乱码

必须设置:

复制代码
const decoder = new TextDecoder('utf-8', { stream: true })

6.3 页面卡顿

  • 未做渲染节流
  • 未做自动滚动优化
  • 内容过大未做虚拟滚动

6.4 调试方法

  • Chrome F12 → Network → Response 查看分块
  • 使用 curl -N http://xxx 测试服务端流是否正常
  • 查看是否触发 cors / 403 / 404 等错误

七、总结

前端流式输出是现代前端必备技能,尤其在 AI 交互、实时消息、日志展示等场景必不可少。

核心三件套:

  1. Fetch + ReadableStream 读取流
  2. TextDecoder 解码
  3. 逐段渲染 + 防抖 + 安全转义

掌握本文内容,你可以:

  • 实现 ChatGPT 式打字机效果
  • 完成实时日志、监控大屏
  • 优化大文本加载速度
  • 写出生产可用的流式组件
相关推荐
不听话坏1 小时前
Ignition篇(下 一) 动态执行前的事情
开发语言·前端·javascript
likeyi071 小时前
require 和 import的区别
开发语言·前端
pany2 小时前
做 AI 友好的开源 Vue3 模板 🌈
前端·vue.js·ai编程
小二·2 小时前
React 19 + Next.js 15 现代前端开发实战:App Router / Server Components / 流式渲染
前端·javascript·react.js
谙忆10243 小时前
前端图片直传对象存储:OSS/S3 预签名 URL、STS 临时凭证与回调校验
前端
CHNE_TAO_EMSM4 小时前
Android studio 打开文件时自动下载源码
前端·javascript·android studio
一孤程5 小时前
Airtest自动化测试第五篇:小程序与Web测试——跨平台自动化全覆盖
前端·自动化测试·小程序·自动化·测试·airtest
IT_陈寒5 小时前
SpringBoot自动配置不是你以为的那样的智能
前端·人工智能·后端
yume_sibai6 小时前
大屏数据可视化 - 边框红绿呼吸灯实现详解
前端·信息可视化·typescript
Hyyy6 小时前
很多Desktop都在上的Computer Use是什么
前端·llm