在一个ai问到聊天页面中,我遇到过一个很典型的流式接口现象:同一个接口在 demo 项目中很快就能收到第一条 SSE 消息,在当前 Taro H5 项目里却会长时间 Pending,随后把积累的内容一次性显示出来。
最终,在本地开发服务器中配置下面这一行后,两个项目的表现一致了:
ts
// config/dev.ts
export default {
h5: {
devServer: {
compress: false,
proxy: {
"/api": {
target: "http://xxx.xx.xx",
changeOrigin: true,
},
},
},
},
};
这看起来像是一个前端小配置,实际暴露的是 SSE 与 HTTP 响应压缩之间的兼容性问题。
SSE 的关键不是"请求成功",而是"每个分片都能立刻到达浏览器"
普通 HTTP 接口通常会等待响应完整生成后再返回;SSE 则会在同一条 HTTP 连接上持续输出事件:
text
event: progress
data: {"text":"正在分析您的症状信息"}
event: message.delta
data: {"text":"请问"}
event: message.delta
data: {"text":"症状持续多久了?"}
浏览器的 ReadableStream 只有在收到字节时,reader.read() 才会继续执行。因此,流式体验依赖整条链路都不缓存、不聚合:
text
浏览器 → Taro/Webpack 开发服务器 → 代理 → SSE 服务
只要其中一层攒住了分片,前端就会表现为"Pending 很久,然后一次性返回"。前端的 SSE 解析器即使写得完全正确,也拿不到尚未抵达浏览器的数据。
compress: true 为什么可能破坏流式体验
Webpack Dev Server 的 devServer.compress 会为可压缩响应启用 gzip / Brotli 等响应压缩中间件。对 HTML、JS、JSON 这类完整响应而言,这能减少传输体积;但 SSE 的每个事件往往很短,例如几十个字节。
压缩器为了提高压缩效率,可能会在内部累计数据后再输出压缩块。于是服务端虽然已经 flush 了事件,中间的压缩层仍可能没有把足够的数据交给浏览器。浏览器只能在压缩数据真正输出、并解压后,才收到一批完整的 SSE 文本。
所以这里的根因并不是 fetch、ReadableStream 或 React 渲染慢,而是开发代理在 SSE 数据通路上增加了一个可能聚合输出的响应压缩层。
关闭该层后:
ts
devServer: {
compress: false,
}
上游返回的 SSE 分片会更直接地透传到浏览器,第一条事件就能及时触发前端回调。此次"关闭后行为与 demo 一致"也说明,demo 与当前项目的主要差异是在本地开发服务器 / 代理层,而不是后端接口本身。
这不是请求参数 compress
这里的 compress: false 是 Webpack Dev Server 的响应压缩配置 ,作用于本地 H5 开发服务器;它不是 SSE 接口请求体或 fetch 的参数。
text
config/dev.ts
└── h5.devServer.compress = false
因此它只解决本地开发环境通过 Taro Webpack 代理访问流式接口时的缓冲问题,不会自动修复生产环境中的网关、Nginx、CDN 或后端响应缓冲。
生产环境如何正确处理 SSE
不建议为了一个 SSE 接口关闭全站压缩。更稳妥的做法是仅对 SSE 路径禁用压缩和缓冲,并保留其他静态资源、普通 API 的压缩能力。
以 Nginx 为例,SSE 路径通常需要类似的策略:
nginx
location /ihis/pre-consult/stream {
proxy_pass http://pre_consult_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
gzip off;
proxy_cache off;
}
服务端响应也应满足:
http
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no
其中 X-Accel-Buffering: no 对经过 Nginx 的响应尤其有用;最终是否生效仍取决于网关和反向代理的实际配置。
排查"流式变成批量返回"的步骤
- 先直接访问后端 SSE 地址,使用
curl -N或服务端日志确认首个事件的实际发送时机。 - 再通过本地代理访问,比较首个事件到达时间;如果只有代理路径变慢,问题在开发服务器或网关层。
- 查看响应头中的
Content-Encoding。出现gzip、br时,要重点检查压缩中间件是否在缓冲分片。 - 确认 Nginx / 网关的
proxy_buffering、缓存和压缩策略。 - 最后再检查前端:使用增量解析器按 SSE 空行(
\n\n)切分事件,避免等到整个响应结束才处理。
总结
SSE 是端到端的流式协议。后端已经逐条 write,并不代表浏览器会逐条收到;任何压缩、缓存或代理缓冲层都可能把它重新变成批量响应。
当前项目在开发环境将 h5.devServer.compress 设为 false,是让预问诊流式接口恢复首包实时性的正确修复。上线前则应将相同原则落实到生产网关:SSE 路由禁用压缩与缓冲,普通业务接口仍保留性能优化。