JSON HTTP 传输:Gzip、Brotli 与流式响应的选择
接口返回 JSON 时,删除空白、把 JSON 再包进字符串,以及使用 HTTP 压缩解决的不是同一件事。本文只讨论 JSON 在 HTTP 上传输的体积和交付方式:如何理解 Content-Encoding、何时分页,以及何时采用 NDJSON。本文不讨论 JSON.parse 的主线程渲染策略。
一、问题概述:传输层、文本层与转义层不要混在一起
格式化 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。验证时检查真实响应头,并覆盖流尾和错误响应。
来源与延伸阅读
技术审校所依据的规范与权威参考资料。
相关文章
API 契约 Breaking Changes:字段变化检查方法
以响应 JSON 契约为范围,区分字段新增、删除、类型、必填和枚举变化,提供不依赖未定义库的 TypeScript 比较示例与发布前验证步骤。
最佳实践OpenAPI/Swagger Schema 到 TypeScript 契约:代码生成与运行时校验
使用 OpenAPI 术语说明 schema、required、nullable、请求/响应契约到 TypeScript 的生成边界,并区分静态代码生成与运行时校验。
最佳实践JSON 转 XML 与 SOAP Web Service 桥接:Envelope、Body 和命名空间
从 JSON 业务输入生成受控的 SOAP 1.1 Envelope,说明 Body、namespace、Fault 与 HTTP 协议边界,避免把普通 XML 转换误称为完整 SOAP 客户端。
继续阅读
可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。
打开关联工具