流式响应前端渲染SSE实战教程

AI 应用前端流式响应渲染与中断恢复最佳实践

2026-09-05 · 约 10 分钟阅读

---

title: "AI 应用前端流式响应渲染与中断恢复最佳实践"

description: "AI应用前端与质量工程:详解流式响应渲染、测试Mock、用户反馈数据飞轮等工程实践。附前端代码模板、测试用例设计与数据闭环方案,帮你提升AI应用交付质量,适合前端工程师与QA团队,含中转站实测数据,附2026年最新API价格,含可直接复用的代码片段,覆盖中转站稳定性对比,是开发者必读的实战指南。"

date: "2026-09-05"

tags: ["流式响应", "前端渲染", "SSE", "实战教程"]

---

# AI 应用前端流式响应渲染与中断恢复最佳实践

后端把 LLM 的流式响应(SSE)正确地吐给浏览器只是第一步——前端如何把"半个字半个字"到达的 Markdown 流畅渲染出来,才是真正考验工程能力的地方。本文从前端视角系统讲解流式响应的渲染、断线恢复、可访问性、性能优化。

一、流式响应带来的前端挑战

和普通 HTTP 请求不同,SSE 的特点是:

  • 响应是持续到达的,content-length 未知
  • 内容是片段化的,一个完整的 Markdown 块可能跨多个 chunk
  • 用户随时可以打断(停止按钮、关闭页面、切走标签)
  • 网络可能中途断开(弱网、切换 WiFi)

这意味着前端不能再用传统的"拿到完整数据 → 一次性渲染"思路,必须设计成"增量渲染 + 中断可恢复"。

二、Markdown 渐进式渲染的核心问题

流式 Markdown 是前端工程师的噩梦:

```

第一个 chunk: "# 你好,我"

第二个 chunk: "们来\n## 第一章"

第三个 chunk: "\n这是内容..."

```

如果在每个 chunk 后都尝试解析和渲染,会遇到:

  • 半个 token 的代码块(```` ` ```` 或三个反引号没闭合)
  • 没结束的表格行(缺 `|`)
  • 没闭合的链接 `[xxx](`
  • 没闭合的粗体/斜体标记

暴力地每收到一个 chunk 就用 marked/react-markdown 重新渲染整段,在长文本场景下会非常卡顿(每次 O(n) 解析 + 大量 DOM diff)。

三、三种渲染策略对比

策略 1:节流批量渲染

每收到一个 chunk 不立刻渲染,而是攒一批再统一刷新:

```javascript

let buffer = "";

let renderScheduled = false;

const FLUSH_INTERVAL = 50; // 50ms 节流

function onChunk(text) {

buffer += text;

if (!renderScheduled) {

renderScheduled = true;
setTimeout(flushRender, FLUSH_INTERVAL);

}

}

function flushRender() {

renderScheduled = false;

// 用 buffer 重新渲染整段 Markdown

// marked 解析 + React 渲染(实际是 vDOM diff,只更新变化部分)

setMarkdown(buffer);

buffer = "";

}

```

优势:实现简单,60fps 流畅

劣势:Markdown 解析器需要对不完整输入宽容(marked/remark 都有 `streaming-friendly` 选项)

策略 2:增量 Token 替换

放弃 Markdown 实时解析,把渲染拆成"已渲染区域"+"待渲染区域":

```javascript

// 已渲染(从 buffer 截取完整 Markdown 块)

// 待渲染(最新的未完成部分,原样显示)

const { rendered, pending } = splitByCompleteBlocks(buffer);

{/* 完整 Markdown 块,用 marked 解析 */}

{/* 未完成块,原样显示 */}

{pending}

```

优势:渲染开销极小(已渲染部分不重新解析)

劣势:未完成块的样式不完整(如代码块没颜色),视觉上有"突变"

策略 3:使用专门的流式 Markdown 库

2026 年已经有成熟的流式 Markdown 库,专门处理不完整输入:

```javascript

import { Streamdown } from 'streamdown';

content={streamingContent}

isAnimating={true} // 显示光标

components={{

code: SyntaxHighlighter,
a: ExternalLink

}}

/>

```

代表项目:`Streamdown`(Vercel)、`react-markdown` 的 `streaming-friendly` 模式。

推荐:生产环境优先用现成的流式 Markdown 库,自己造轮子性价比低。

四、代码块的高亮与复制

流式场景下的代码高亮要小心:

```javascript

// ❌ 错误做法:每个 chunk 都重新高亮整段代码

// 代码越长,重新高亮的成本越高(highlight.js 是 O(n))

// ✅ 正确做法:代码块闭合后再高亮

function tryHighlightCodeBlock(buffer) {

// 检测到 ``` 闭合标记才解析

if (buffer.match(/```\n/g)?.length % 2 === 0) {

// 偶数个 ``` 表示所有代码块都闭合
return hljs.highlight(buffer);

}

return null; // 暂不高亮,原样显示

}

```

代码块右上角的"复制"按钮在流式未完成时建议禁用,避免复制到半成品代码:

```javascript

function CodeBlock({ language, code, isComplete }) {

return (

{code}

);

}

```

五、中断与恢复机制

5.1 主动停止

用户点击"停止生成"时,前端要做三件事:

```javascript

function stopGeneration() {

// 1. 关闭 EventSource 连接

eventSource.close();

// 2. 通知后端停止(可选,避免无意义的 token 消耗)

fetch('/api/stop', { method: 'POST', body: JSON.stringify({ request_id }) });

// 3. 标记当前消息状态为"已中断"

updateMessage(messageId, { status: 'aborted', content: currentBuffer });

}

```

5.2 被动中断:网络断开

弱网环境下 SSE 连接经常意外断开,需要自动重连:

```javascript

function connectWithRetry(url, options) {

let retryCount = 0;

const MAX_RETRIES = 5;

function connect() {

const es = new EventSource(url);
es.onopen = () => {
  retryCount = 0;  // 连接成功,重置重试计数
};
es.onerror = () => {
  es.close();
  if (retryCount < MAX_RETRIES) {
    retryCount++;
    const delay = Math.min(1000 * 2 ** retryCount, 30000);  // 指数退避
    setTimeout(connect, delay);
  } else {
    showError("网络不稳定,已停止重试");
  }
};
es.onmessage = (event) => {
  onChunk(event.data);
};

}

connect();

}

```

更优雅的方案是用 `@microsoft/fetch-event-source` 库(支持 POST、自定义 header、自动重连)。

5.3 中断后恢复显示

网络恢复后,前端要平滑地把已收到的内容"接上",不能让用户感觉是两条断开的对话。

建议前端设计"会话快照"机制:

```javascript

// 每收到一个 chunk 就把当前 buffer 存到 sessionStorage

// 断线恢复时从 sessionStorage 恢复 buffer,再继续拼接新 chunk

function saveSnapshot(sessionId, buffer) {

sessionStorage.setItem(`chat:${sessionId}:buffer`, buffer);

}

// 页面刷新或断线恢复时

const savedBuffer = sessionStorage.getItem(`chat:${sessionId}:buffer`);

if (savedBuffer) {

appendToMessage(savedBuffer); // 把之前的内容先显示出来

}

```

六、性能优化清单

1. 节流渲染:50-100ms 节流一次 Markdown 渲染,避免每 chunk 都触发完整 diff

2. 虚拟滚动:超长对话(> 100 轮)用虚拟滚动,只渲染可视区域

3. 代码块懒高亮:代码块闭合后再 highlight,正在写入时用纯文本

4. 图片懒加载:AI 生成的 Markdown 里可能含图片,用 `loading="lazy"`

5. 避免布局抖动:流式过程中高度变化会导致页面跳动,给容器设 `min-height`

6. Service Worker 缓存:相同 Prompt 的请求结果本地缓存(注意隐私合规)

七、可访问性(A11y)

流式响应给屏幕阅读器用户带来挑战:

```html

AI 回复已完成

```

`aria-live="polite"` 让屏幕阅读器在合适时机朗读新内容;`aria-atomic="false"` 表示只朗读新增部分(避免整段重读)。

代码块的复制按钮需要键盘可访问:

```jsx

onClick={handleCopy}

aria-label="复制代码"

onKeyDown={(e) => e.key === 'Enter' && handleCopy()}

>

复制

```

八、可观测性:前端也要埋点

前端流式渲染的关键指标:

  • TTFT(Time To First Token,首 token 延迟)
  • 首屏渲染延迟(从首 token 到用户能看到的时间)
  • 渲染卡顿率(FPS < 30 的比例)
  • 中断率(用户主动停止的比例)
  • 断线重连成功率

用 Performance Observer API 采集这些指标上报:

```javascript

const observer = new PerformanceObserver((list) => {

for (const entry of list.getEntries()) {

if (entry.entryType === 'longtask') {
  analytics.track('long_task', { duration: entry.duration });
}

}

});

observer.observe({ entryTypes: ['longtask'] });

```

总结

前端流式渲染的体验直接决定用户对 AI 应用的第一印象。核心建议:

  • 用成熟的流式 Markdown 库(如 Streamdown),不要自己造轮子
  • 50-100ms 节流批量渲染,避免每 chunk 都触发完整重渲染
  • 代码块闭合后再高亮,未完成时禁用复制按钮
  • 实现指数退避的自动重连 + sessionStorage 快照恢复
  • 别忘了可访问性,给屏幕阅读器用户友好体验

实际项目落地时,建议选用支持完整 SSE 兼容、按 token 计费稳定的中转服务商,能省去大量后端适配工作。可以在 [openairouter.net](https://openairouter.net) 排行榜里查看各家在流式响应稳定性上的实测数据。

找到最适合你的 AI API 中转站

收录 125+ 服务商,按价格、模型、标签一键筛选

查看所有中转站 →