一行 `compress: false`,为什么让 SSE 首包恢复实时返回

在一个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 文本。

所以这里的根因并不是 fetchReadableStream 或 React 渲染慢,而是开发代理在 SSE 数据通路上增加了一个可能聚合输出的响应压缩层。

关闭该层后:

ts 复制代码
devServer: {
  compress: false,
}

上游返回的 SSE 分片会更直接地透传到浏览器,第一条事件就能及时触发前端回调。此次"关闭后行为与 demo 一致"也说明,demo 与当前项目的主要差异是在本地开发服务器 / 代理层,而不是后端接口本身。

这不是请求参数 compress

这里的 compress: falseWebpack 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 的响应尤其有用;最终是否生效仍取决于网关和反向代理的实际配置。

排查"流式变成批量返回"的步骤

  1. 先直接访问后端 SSE 地址,使用 curl -N 或服务端日志确认首个事件的实际发送时机。
  2. 再通过本地代理访问,比较首个事件到达时间;如果只有代理路径变慢,问题在开发服务器或网关层。
  3. 查看响应头中的 Content-Encoding。出现 gzipbr 时,要重点检查压缩中间件是否在缓冲分片。
  4. 确认 Nginx / 网关的 proxy_buffering、缓存和压缩策略。
  5. 最后再检查前端:使用增量解析器按 SSE 空行(\n\n)切分事件,避免等到整个响应结束才处理。

总结

SSE 是端到端的流式协议。后端已经逐条 write,并不代表浏览器会逐条收到;任何压缩、缓存或代理缓冲层都可能把它重新变成批量响应。

当前项目在开发环境将 h5.devServer.compress 设为 false,是让预问诊流式接口恢复首包实时性的正确修复。上线前则应将相同原则落实到生产网关:SSE 路由禁用压缩与缓冲,普通业务接口仍保留性能优化。

相关推荐
海边的云1 小时前
别再让 AI 瞎改代码:一套让大模型「收敛」的前端专家 Skill》
前端
kisshyshy1 小时前
给端侧大模型装上“发动机”:React 合成事件 + 进度条组件全解
前端·react.js·node.js
hunterandroid1 小时前
DataStore 工程化实践:迁移、并发更新与异常恢复
android·前端
晓说前端1 小时前
TypeScript 核心语法进阶 —— 字面量类型与类型推论
前端·typescript
不简说2 小时前
JS 代码技巧 vol.8 — 20 个函数式编程实战,把 if/else 拍扁的骚操作
前端·javascript·面试
头茬韭菜2 小时前
4.9 SSRF 防护 — Web 工具的出站安全与私有 IP 拦截
前端·tcp/ip·安全
Cobyte2 小时前
模板 DSL 解析器中的状态机设计
前端·javascript·vue.js
颜酱2 小时前
# 02 | 搭骨架:用 LangGraph 编排 12 步工作流(思路)
前端·人工智能·后端
颜酱2 小时前
02 | 搭骨架:用 LangGraph 编排 12 步工作流
前端·人工智能·后端