sse推ai长文本卡顿或断连主因是浏览器和中间件对text/event-stream容忍度低于文档预期;必须设置4个关键响应头:content-type、cache-control、connection、x-accel-buffering,缺一即导致中断或缓存。

为什么SSE推AI长文本会卡顿或断连
不是代码写错了,而是浏览器和中间件对 text/event-stream 的实际容忍度比文档写的低得多。常见现象包括:前端 EventSource 随机触发 error 事件、Chrome 控制台报 net::ERR_INCOMPLETE_CHUNKED_ENCODING、Nginx 默认 60 秒断连、长文本下每字 flush 导致 DOM 频繁重绘卡死。
Go SSE Handler 必须设的 4 个响应头
少一个都可能让流中断或被缓存:
-
w.Header().Set("Content-Type", "text/event-stream")—— 不是application/json,否则浏览器不识别为流 -
w.Header().Set("Cache-Control", "no-cache")—— Safari 会缓存第一个 chunk,后续数据全丢 -
w.Header().Set("Connection", "keep-alive")—— 显式声明,绕过某些代理的连接复用策略 -
w.Header().Set("X-Accel-Buffering", "no")—— Nginx 专用,禁用其内部缓冲,否则卡住 1–3 秒才吐出第一段
从 AI 接口读 chunk 并转成 SSE 的正确姿势
别等整个响应体读完再发;也别用 json.Decoder 直接解大 JSON 流——它会阻塞在不完整字段上。正确做法是按行切分 event-stream:
- 用
bufio.Scanner{Split: bufio.ScanLines}逐行读resp.Body - 跳过空行和
event:、id:等控制行,只处理data: {...} - 对每个
data:行,用strings.TrimPrefix(line, "data: ")提取 JSON 字符串 - 用
json.Unmarshal解出delta.content或choices[0].delta.content,再封装成你自己的SSEMessage结构 - 每次写完必须调
flusher.Flush(),且建议加time.Sleep(1 * time.Millisecond)防止写太快压垮客户端渲染线程
前端 EventSource 拿到 token 后怎么避免页面卡死
后端推得快,不代表前端能跟上。高频 data: 消息直接触发 DOM 更新,会导致主线程饱和:
- 不要每收到一个
data就element.textContent += token - 改用节流缓冲:用
setTimeout或requestIdleCallback聚合 20ms 内所有 token,一次性更新 - 聊天容器必须设
max-height+overflow-y: auto,否则高度重算引发布局抖动 - 如果用户快速滚动到底部,新消息进来时别自动
scrollIntoView,先判断是否已在可视区,否则强制滚动会打断操作
真正难的不是推流,是让流“稳”——从 Nginx 缓冲、Go 的 flush 时机、到前端渲染节奏,每一环都有隐性瓶颈。最容易被忽略的是 X-Accel-Buffering 和前端节流,这两处不处理,其他优化全白搭。











