你的API为什么慢?我花了一周排查,结果是个空格惹的祸
说出来你可能不信,我上周被一个接口性能问题折腾了整整一周,最后发现罪魁祸首是一个空格。
不是在代码里的空格,是一个JSON序列化时多出来的空格。
事情是这样的——我们有个数据导出接口,导出1000条数据,耗时居然要8秒。数据库查询只要200毫秒,剩下的时间全消失在「序列化」这个环节。
今天就把这个排查过程和中间学到的JSON序列化反模式分享出来,保证比你看那些"JSON入门教程"有用一万倍。
第一坑:字符串拼接是最慢的序列化方式
先问个问题:下面两种JSON构建方式,哪个更快?
// 方式一:字符串拼接
let json = '{';
json += '"name": "张三",';
json += '"age": 25';
json += '}';
// 方式二:对象序列化
let data = { name: "张三", age: 25 };
let json = JSON.stringify(data);
你的直觉可能是方式一更快——看起来更"可控",没有抽象层。
但实际上,方式二比方式一快5到10倍。
原因很简单:JSON.stringify是C++实现的,而字符串拼接是JavaScript引擎的逐字符操作。对于小于1KB的数据差异不明显,但当你处理大型数据集时,这个差异会放大到难以忍受的程度。
我们那个导出接口,最初的实现就是用字符串拼接拼出来的「自定义JSON」。当时觉得这样可以控制格式,结果——8秒。
第二坑:toJSON方法里的陷阱
很多人知道可以给对象定义toJSON方法来定制序列化行为,但不知道这里有个坑:toJSON里别再调用JSON.stringify了。
// 错误示例
const user = {
name: "张三",
profile: { bio: "程序员", skills: ["JavaScript", "Python"] },
toJSON() {
return JSON.stringify({
name: this.name,
...this.profile
});
}
};
// 序列化结果:profile字段变成了字符串,而不是对象!
// 正确示例
const user = {
name: "张三",
profile: { bio: "程序员", skills: ["JavaScript", "Python"] },
toJSON() {
return {
name: this.name,
bio: this.profile.bio,
skills: this.profile.skills
};
}
};
第一眼看到这个bug的时候,我人傻了。profile字段在序列化后从对象变成了字符串「{"bio":"程序员","skills":["JavaScript","Python"]}」。
这直接导致前端解析出错,因为代码里假设profile是个对象。这属于那种「语法完全正确,逻辑完全错误」的bug,极难排查。
第三坑:循环引用和undefined的序列化
JSON.stringify有两个特性很多人不知道:
- 循环引用的对象会抛出异常
- undefined、Symbol、函数会被自动忽略
第一个坑比较明显,第二个坑就比较隐蔽了:
const data = {
name: "张三",
age: undefined, // 序列化后消失
gender: null, // 序列化为 null
phone: Symbol("phone"), // 序列化后消失
greet: function() {} // 序列化后消失
};
console.log(JSON.stringify(data));
// 输出: {"name":"张三","gender":null}
你以为你传了一个完整的对象给前端,其实中间有些字段已经默默消失了。如果前端代码假设某个字段一定存在,轻则页面异常,重则数据库写入失败。
更阴的是,这种问题在测试环境可能不会出现——因为测试数据往往比较「干净」,而生产数据可能有各种边缘值。
第四坑:大数字精度丢失
JSON规范里没有整数和浮点数的区别,只有「数字」。而JavaScript的Number类型使用IEEE 754双精度浮点数,能安全表示的整数范围是 -(2^53-1) 到 (2^53-1)。
换句话说:如果你有个ID是 9007199254740993,JSON序列化后会变成 9007199254740992。
const bigId = 9007199254740993;
console.log(JSON.parse(JSON.stringify({ id: bigId })).id);
// 输出: 9007199254740992 —— 少了1!
这个问题在高并发系统中很常见——订单ID、交易ID、用户ID都可能超过这个范围。
解决方案有两个:
- 用字符串传递大数字:数据库里存字符串,前端也用字符串
- 用JSONBig库:支持大数字的序列化
import JSONBig from "json-bigint";
const data = { orderId: 9007199254740993 };
const json = JSONBig.stringify(data);
// 输出: {"orderId":"9007199254740993"}
const parsed = JSONBig.parse(json);
// 输出: { orderId: BigInt "9007199254740993" }
如果你用的是Node.js,还有一个更狠的问题:雪花算法生成的ID在JSON序列化时会精度丢失,而很多团队用了雪花算法很久都不知道这个问题,直到某天客服开始收到用户反馈「订单号查不到」。
第五坑:空格——我这次踩的坑王
好了,终于到正题了。
我们那个8秒的导出接口,序列化用了6秒。代码是这么写的:
function buildJson(rows) {
let json = '[';
rows.forEach((row, index) => {
json += ' { ';
json += '"id": ' + row.id + ', ';
json += '"name": "' + row.name + '", ';
json += '"email": "' + row.email + '" ';
json += '}';
if (index < rows.length - 1) json += ', ';
});
json += ']';
return json;
}
注意到了吗?每个对象里的字段之间有空格:「 "id": 1, 」。
对于1000条数据,每条数据10个字段,就是10000个空格。而每行代码里的+操作符和字符串拼接,会产生大量的中间字符串对象。
修复后的代码:
function buildJson(rows) {
return '[' + rows.map(row =>
`{"id":${row.id},"name":"${row.name}","email":"${row.email}"}`
).join(',') + ']';
}
时间从8秒降到了400毫秒。20倍提升,就因为少了一些空格和一次JOIN操作。
当然,这个例子的空格问题是人为制造的。但真实场景中,JSON序列化慢的原因往往是:
- 不必要的嵌套结构
- 序列化不需要返回给前端的字段
- 大字段(如text类型的文章内容)没有单独处理
第六坑:循环中的序列化
这是一个经典反模式:
// 错误:循环中每次都序列化
const users = [{ name: "张三" }, { name: "李四" }];
users.forEach(user => {
console.log(JSON.stringify(user));
});
// 正确:批量处理
console.log(JSON.stringify(users));
原理很简单:JSON序列化的启动开销不小,循环调用会重复这个开销。如果你要处理1000个对象,循环序列化可能比一次性序列化慢几十倍。
第七坑:不知道有替代方案
JSON不是唯一的序列化格式。在特定场景下,有更快或更小的选择:
- MessagePack:二进制格式,比JSON小30-50%,解析快2-3倍
- Protocol Buffers:Google的序列化协议,需要定义schema,但性能极高
- CBOR:类似MessagePack,专为嵌入式设计
import msgpack from "@msgpack/msgpack";
const data = { name: "张三", age: 25 };
const packed = msgpack.encode(data);
const unpacked = msgpack.decode(packed);
如果你在处理海量数据的场景,MessagePack能给你带来显著的网络传输和解析性能提升。
总结:JSON序列化的七条军规
写到这里,给你七条我在血泪教训中总结的规则:
- 永远用JSON.stringify,而不是字符串拼接——除非你在写面试题
- toJSON方法返回对象,不是字符串——这是个很容易犯的错
- 注意undefined和函数会消失——测试时用边缘值
- 大数字用字符串或JSONBig——雪花ID必看
- 避免循环中序列化——批量处理是基本素养
- 选择合适的序列化格式——JSON不是万能的
- 永远不要手动拼接JSON——除非你想体验8秒的恐惧
JSON看起来简单,但里面的坑足够你喝一壶。希望我这一周的排查经历能让你少走点弯路。
如果你也有类似的踩坑经历,欢迎来comck.com和我聊聊。