Javascript is required
最佳实践发布于 2026-07-28审校于 2026-08-086 分钟阅读

JSON HTTP 传输:Gzip、Brotli 与流式响应的选择

接口返回 JSON 时,删除空白、把 JSON 再包进字符串,以及使用 HTTP 压缩解决的不是同一件事。本文只讨论 JSON 在 HTTP 上传输的体积和交付方式:如何理解 Content-Encoding、何时分页,以及何时采用 NDJSON。本文不讨论 JSON.parse 的主线程渲染策略。

HTTPContent-EncodingGzipBrotliNDJSON

一、问题概述:传输层、文本层与转义层不要混在一起

格式化 JSON 的空白可以删除,但这只是改变文本表示;转义用于把文本嵌入另一段字符串,通常会增加字符。Gzip 和 Brotli 则由 HTTP 的 Content-Encoding 协商,客户端通常在读取响应前已完成透明解压。应先确认瓶颈是网络传输、响应体选择过大,还是消费端处理方式,再选择手段。

二、最小复现:响应头决定浏览器如何接收编码内容

服务端返回 br 或 gzip 时,应同时提供正确的 Content-Type、Content-Encoding 和 Vary。浏览器中的 fetch 返回可读取的解压后响应体;不要把正常 fetch 得到的文本再当作 Brotli 字节手动解压。

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Encoding: br
Vary: Accept-Encoding

{"items":[{"id":"a1","name":"Ada"}]}

三、根因:Accept-Encoding 是协商,不是 JSON 字段

客户端会在请求中声明可接受的编码,服务端或 CDN 决定是否选择其中一种并在响应头标明结果。压缩算法和等级、内容重复度、缓存策略以及代理配置都会影响结果,因此不能预先承诺固定压缩率或耗时。若代理按 Accept-Encoding 返回不同表示,Vary 能避免共享缓存把错误版本交给其他客户端。

四、推荐选择:先减少不需要的数据,再选择交付形式

常规 JSON API 可由服务端启用 Gzip 或 Brotli,并保留可读的 JSON 语义。一个响应本身不应包含客户端不需要的完整集合:使用游标分页或字段投影缩小响应。只有当服务端能逐条产出、客户端也能逐条消费记录时,才考虑 application/x-ndjson;它不是任意 JSON 数组的替代格式。

  • 小而完整的对象:普通 JSON 加 HTTP 内容编码。
  • 可拆分的集合:游标分页或按需字段。
  • 连续产生的独立记录:NDJSON,并明确每一行都是完整 JSON。

五、完整示例:在浏览器中安全读取 NDJSON

下面代码运行于现代浏览器。它检查状态与 Content-Type,保留跨网络分块的半行,并在流结束后处理最后一行。回调负责消费记录,不假设 UI 如何渲染。

async function readNdjson(
  url: string,
  onItem: (item: unknown) => void,
): Promise<void> {
  const response = await fetch(url, { headers: { Accept: "application/x-ndjson" } });
  if (!response.ok) throw new Error("HTTP " + response.status);
  if (!response.body) throw new Error("ReadableStream is unavailable");

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let pending = "";

  const consume = (line: string) => {
    if (line.trim() !== "") onItem(JSON.parse(line));
  };

  while (true) {
    const chunk = await reader.read();
    if (chunk.done) break;
    pending += decoder.decode(chunk.value, { stream: true });
    const lines = pending.split("\n");
    pending = lines.pop() ?? "";
    for (const line of lines) consume(line.endsWith("\r") ? line.slice(0, -1) : line);
  }
  pending += decoder.decode();
  consume(pending.endsWith("\r") ? pending.slice(0, -1) : pending);
}

六、常见错误方案

把 JSON stringify 后再作为 JSON 字符串字段传输会引入额外转义和第二次解析,除非协议确实要求嵌套文本。把压缩后的二进制自行 Base64 化通常会扩大文本并绕过 HTTP 缓存/协商机制。把任意 JSON 数组按换行切开也不正确:字符串值可含换行转义,只有 NDJSON 协议才保证一行一条记录。

七、边界条件:缓存、错误响应与流尾

错误响应也可能是 HTML 或普通文本,读取前应检查 response.ok 和 Content-Type。NDJSON 的最后一行可能没有换行符,TextDecoder 还可能保留未完成的多字节字符,因此需要在结束时调用 decoder.decode()。若中间记录本身不是合法 JSON,应记录该行并决定是中止还是由协议定义错误记录,不能静默吞掉。

八、如何验证交付方式

在浏览器 Network 面板确认请求的 Accept-Encoding、响应的 Content-Encoding 与 Vary;再比较同一资源在关闭和开启压缩时的响应头,而不是套用别人的压缩数字。对 NDJSON,可用包含分块行、空行、CRLF 和无末尾换行的数据调用 readNdjson,断言回调顺序和值都正确。

九、FAQ

问:Brotli 一定比 Gzip 更适合 API 吗?答:取决于服务器、客户端支持、缓存和内容;应让部署环境协商并测量自身资源。

问:压缩后能否省略分页?答:不能。压缩减少传输字节,但不会改变客户端最终需要处理的数据量。

问:NDJSON 能表示嵌套对象吗?答:可以;限制是每一条顶层记录必须独立占一行,记录内部仍可包含对象和数组。

十、总结

JSON 空白消除、转义与 HTTP 内容编码属于不同层。常规接口优先使用标准 JSON 配合服务器协商的 Gzip 或 Brotli;响应过大时先减少数据,只有逐条交付确有价值时才使用 NDJSON。验证时检查真实响应头,并覆盖流尾和错误响应。

来源与延伸阅读

技术审校所依据的规范与权威参考资料。

相关文章

继续阅读

可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。

打开关联工具