做过移动端或者前后端分离项目的工程师,大概率遇到过这样的困扰:一个列表接口返回的数据动辄几百KB甚至上兆,打开响应内容一看,满屏都是重复的键名和层层嵌套的空结构。数据格式臃肿带来的不只是流量浪费,更直接影响了接口响应时间、客户端解析耗时以及服务端带宽成本。要解决这个问题,需要从数据结构本身和传输编码两个层面同时下手。

一、先搞清楚数据为什么臃肿
JSON之所以成为臃肿的重灾区,和它的文本特性直接相关。JSON把键名以明文形式重复写在每一个对象里,假设一个商品对象有十个字段,返回一千个商品,那么像productName、createTime这样的键名就要重复一千次。以一个实际例子来看:
[
{"productId": 10001, "productName": "无线蓝牙耳机", "price": 199.00, "stockQuantity": 350, "createTime": "2023-06-01 10:23:45"},
{"productId": 10002, "productName": "机械键盘", "price": 329.00, "stockQuantity": 120, "createTime": "2023-06-02 14:11:30"}
]上面这段数据里,键名占掉的字符数接近一半。当列表规模扩大到几千条时,冗余比例还会进一步放大。除此之外,时间戳被格式化成字符串、数字被保留多余的小数位、空对象和空数组层层嵌套,都是常见的体积杀手。
另一个容易被忽视的问题是字段冗余。很多接口直接把数据库实体序列化后返回,导致客户端根本用不到的字段也被一并传输。一次接口改造中我曾做过统计,某详情接口返回了六十多个字段,前端实际使用的只有十一个,冗余率超过八成。
二、压缩方案:gzip与brotli的选择
压缩是最不需要改动业务代码的优化手段。gzip基于DEFLATE算法,对文本型数据的重复内容识别率很高,JSON里大量重复的键名和结构正好是它擅长处理的场景。一般情况下,gzip对JSON能达到70%到85%的压缩率,效果相当可观。
在Nginx中开启gzip压缩只需要几行配置:
gzip on; gzip_min_length 1k; gzip_comp_level 5; gzip_types application/json text/plain application/javascript; gzip_vary on;
gzip_comp_level的取值范围是1到9,数值越大压缩得越狠,但CPU消耗也越高。经过实测,级别从5提升到9,体积通常只能再减少2%左右,而CPU耗时可能翻倍,线上环境一般建议设置在4到6之间。
如果客户端环境允许,brotli是比gzip更优的选择。它由Google推出,对文本压缩率普遍比gzip再高出15%到25%,浏览器支持度也已经非常成熟。Nginx需要安装ngx_brotli模块后即可启用。需要注意的是,brotli在压缩级别较高时耗时上升明显,适合静态资源预压缩,动态接口则建议用较低级别。
三、序列化格式优化:从文本走向二进制
压缩解决的是传输层的问题,如果想让数据本身变得更小,就需要考虑更换序列化格式。二进制格式把键名替换为简短的字段编号,省去了重复键名的开销,同时数值、布尔值等类型直接以二进制存储,不再以字符串形式占位。
MessagePack是最容易上手的方案,它和JSON的语义几乎一一对应,很多语言的支持库都提供了json与pack之间的直接转换,改造成本低,适合作为渐进式迁移的第一步。它的体积通常比原始JSON小30%到50%。
Protobuf则更进一步,它通过.proto文件严格定义数据结构,字段用编号标识,序列化后的体积可以比JSON小60%以上,解析速度也快一个量级。先来看一个简单的定义:
syntax = "proto3";
message Product {
int32 product_id = 1;
string product_name = 2;
double price = 3;
int32 stock_quantity = 4;
int64 create_time = 5;
}注意定义里的create_time用的是int64,直接传输毫秒时间戳,而不是格式化后的字符串。仅这一项改动,每个商品对象就能省下十几个字节,还避免了客户端解析时间字符串的开销。Protobuf的代价是需要维护proto文件的版本管理和多端代码生成流程,团队协作成本比JSON高,适合对性能敏感、接口相对稳定的内部服务通信。
三种格式的对比如下:
| 格式 | 相对JSON体积 | 解析速度 | 可读性 | 改造成本 |
|---|---|---|---|---|
| JSON | 100% | 基准 | 好 | 无 |
| MessagePack | 50%到70% | 快约2到5倍 | 差 | 低 |
| Protobuf | 25%到40% | 快5到10倍 | 差 | 中高 |
四、不换格式也能瘦身的实用技巧
如果短期内部不具备更换序列化格式的条件,也有不少零成本或低成本的瘦身手段。第一是字段裁剪,为不同场景定义不同的视图对象,列表接口只返回列表渲染需要的字段,详情接口再返回完整数据。配合DTO层的抽象,这个改造可以在服务端独立完成,客户端无感知。
第二是结构扁平化。深层嵌套不仅让JSON体积膨胀,也让解析代码变得繁琐。把不必要的层级压缩掉,用前缀命名代替嵌套,比如把user.address.city扁平化为user_city,在纯数据传输场景下是划算的取舍。
第三是数值类型的收敛。时间统一用时间戳整数,金额用分为单位的整数代替两位小数的浮点数,布尔值用0和1代替true和false字符串。这些细节单个看不明显,在大列表场景下累积效果非常可观。此外,分页参数的合理设置也很关键,与其一次拉取全部数据再前端过滤,不如让服务端按需返回。
最后建议建立一个度量机制,持续监控各核心接口的响应体积。没有数据的支撑,优化就无从判断效果。把接口平均响应大小纳入监控面板,每次版本迭代后对比变化,臃肿问题才不会在不知不觉中卷土重来。压缩、序列化、结构优化三者并不是互斥关系,实际项目中往往叠加使用:服务端用Protobuf编码,传输层再做brotli压缩,多管齐下才能把数据体积压到最低。