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

前言

在 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 式打字机效果
  • 完成实时日志、监控大屏
  • 优化大文本加载速度
  • 写出生产可用的流式组件
相关推荐
sTone873758 小时前
Electron 进程架构模型
前端·electron
ZC跨境爬虫8 小时前
跟着 MDN 学CSS day_25:(高级区块效果)
前端·css·html·tensorflow·媒体
暴躁小师兄数据学院8 小时前
【AI大模型应用开发工程师特训笔记】第04讲(第7章):函数与模块
前端·人工智能·python
跟着珅聪学java8 小时前
ECharts subtext(副标题)边距开发教程
前端·javascript·echarts
哈撒Ki8 小时前
快速入门 Electron
前端·面试·electron
还有多久拿退休金9 小时前
LLM应用开发一:给失忆的大模型装上"脑子"——LangChain.js对话记忆从零实战
前端·llm
思考着亮9 小时前
1.window.location.href 和 router.push 跳转方式
前端
ZengLiangYi9 小时前
插件式架构设计:SourceAdapter 接口抽象
前端·javascript·后端
万岳科技系统开发9 小时前
私域直播系统开发从0到1:企业直播平台搭建全过程
前端·小程序·架构