应按sse标准逐行解析流数据,用textdecoder流式解码防乱码,增量解析markdown并校验结构,节流渲染且仅渲染可视区内容。

直接用 EventSource.onmessage 接收整块数据再解析,容易出错——尤其当模型输出被网络分片截断、Markdown 结构未闭合、或中文字符跨字节断裂时,轻则乱码,重则 DOM 崩溃。关键不是“收到就渲染”,而是把流拆成可管理的语义单元,再安全落地。
按标准 SSE 格式逐行提取有效载荷
SSE 数据流本质是文本行组成的流,每条消息以 data: 开头,以双换行符 \n\n 分隔。不能直接把 event.data 当完整 JSON 用,得先做行级切分:
- 维护一个缓冲区字符串(
buffer),持续追加event.data - 用
buffer.split(/\n\n/)切出完整消息块,保留未闭合的最后一段到缓冲区 - 对每个完整块,去除前缀
data:,再JSON.parse()提取delta.content - 忽略空行、注释行(如
id:、event:)和结束标记[DONE]
防乱码:用 TextDecoder 流式解码(即使不用 fetch)
虽然 EventSource 自动处理 UTF-8,但某些代理或 CDN 可能破坏多字节字符边界。稳妥做法是手动接管解码:
- 不依赖
event.data的自动解码结果 - 改用
fetch+ReadableStream获取原始Uint8Array - 初始化
const decoder = new TextDecoder('utf-8', { stream: true }) - 每次
reader.read()后调用decoder.decode(chunk, { stream: true }),让浏览器自动拼接跨块字符
增量 Markdown 渲染与结构保护
用户看到的是“打字机效果”,但背后必须防止半截语法破坏渲染。不能等整段 Markdown 收齐再解析:
- 将新内容追加到当前消息的累积文本中(
currentText += chunk) - 用轻量 Markdown 解析器(如
marked的parseInline或自研 tokenizer)只解析新增部分 - 检测未闭合结构:比如已出现
```但没遇到下一个```,就暂不渲染代码块,只高亮为普通文本 - 最终渲染时,用
textContent插入纯文本段落,用innerHTML插入已校验安全的 HTML 片段(需先DOMPurify.sanitize())
节流渲染与可视区优化
高频 token 推送(如每秒 20+ 字)直接触发 DOM 更新会卡顿。要让 UI 跟上流速,又不拖垮主线程:
- 用
requestIdleCallback或setTimeout(..., 0)批量合并最近 30ms 内的内容更新 - 限制单次插入长度,超过 50 字符就截断并加“…”提示,避免长段阻塞渲染
- 消息容器启用虚拟滚动(如
react-window或原生IntersectionObserver),只渲染可视区域内的消息项 - 页面失焦(
visibilitychange)时暂停渲染,仅缓存文本,恢复后再批量 flush
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











