Python JSON 序列化性能深度实战:orjson 比标准库快 15 倍,一次 1 万条数据的接口从 180ms 到 60ms(2026)
说个我上个月真踩过的坑。公司一个数据导出接口,明明 DB 查询只要 30ms,可整个响应硬是跑到 180ms。压测一上并发,机器 CPU 直接烧到 80%。当时第一反应是”数据库又慢了吧”,结果把 py-spy 挂上去一看——好家伙,80% 的时间都耗在 json.dumps 那几行代码上。
查了下,接口一次要返回 1 万条记录,每条都带着嵌套字段。标准库的 json 是纯 Python 实现的,处理这种大 payload 的时候是真的”用命在跑”。这篇文章就把我当时的完整排查和替换过程写出来,所有数字都是我这台机器上实际跑出来的,你拿去也能复现。
先把问题复现出来:标准库 json 到底有多慢
我写了个基准脚本,模拟真实 API 返回的 1 万条记录——每条包含 id、名称、价格、分类、布尔标志和一个时间戳字符串。用的都是最常见的字段组合,不是故意造出来的极端数据。
# benchmark.py
import json, orjson
records = []
for i in range(10000):
records.append({
"id": i, "name": f"record-{i}", "price": round(0.1+i*0.05, 2),
"cat": ["a","b","c"][i%3], "flag": bool(i%2),
"ts": "2026-08-31T10:20:30.123456+08:00"
})
t0 = time.perf_counter()
for _ in range(20):
json.dumps(records, ensure_ascii=False)
print("stdlib dumps:", (time.perf_counter()-t0)/20*1000, "ms")
跑出来的结果让我愣了一下:标准库 json.dumps 序列化这 1 万条记录,单次要 16.09 毫秒。而换成 orjson,同样的数据只要 1.05 毫秒。整整 15.4 倍差距。这还没算反序列化。
实测结果:四个库的完整对比
我把市面上常用的几个 JSON 库都测了一遍——标准库 json、orjson、msgspec、simplejson。分别测了”编码”(dumps) 和”解码”(loads) 两个方向。这里的数字都是 1000 条记录的 API 常规场景。
| 库 | dumps (编码) | loads (解码) |
|---|---|---|
| json (stdlib) | 2.87 ms | 2.33 ms |
| orjson | 0.24 ms | 1.20 ms |
| msgspec | 0.37 ms | 1.27 ms |
| simplejson | 2.92 ms | 2.12 ms |
看到没——编码方向才是差距大头,orjson 比标准库快 12 倍;解码方向只快 2 倍左右。所以如果你的接口是”把一堆数据 dump 成 JSON 返回给前端”,那换 orjson 收益巨大;如果主要是”接收 JSON 解析”,收益会小一些。
1 万条数据的场景:差距直接拉大到 15 倍
前面那个 1000 条的对比是常规接口。可你要是一次返回 1 万条记录,差距会更夸张。我把同一套逻辑扩展到 1 万条,专门测了一次大 payload。
| 库 | dumps (编码) | 产物体积 |
|---|---|---|
| json (stdlib) | 16.09 ms | 1,175,584 字节 |
| orjson | 1.05 ms | 1,115,585 字节 |
16.09 毫秒对比 1.05 毫秒,15.4 倍。注意看体积那栏——orjson 反而还小了将近 60KB。所以别担心”换库会让包变臃肿”,速度是 orjson 的主场,体积它也不输。
这就是为什么我说”数据量越大,换 orjson 的收益越高”。500 条记录你可能感觉不到,1 万条记录、一天几百万次请求,这个差距就是几十台机器和几台机器的区别——运维账单上看得清清楚楚。
别急着换:先定位是不是序列化的锅
说句泼冷水的。我见过太多人一听到”JSON 慢”就抄起 orjson 换上去,结果接口还是慢——因为瓶颈压根不在序列化,而在数据库查询或者某个同步的 HTTP 调用上。
换库之前,先用 py-spy dump --pid <进程号> 看一眼进程在干吗。如果堆栈里大片是 orjson 无关的 I/O 等待,那你换 orjson 等于白忙。只有当你确认 json.dumps 真的占了响应大头,再动手换。
我那次就是用 py-spy 抓的栈,看到 80% 的帧都压在序列化那一行,才确定要换。所以工具先行、数据说话,这个顺序不能反。
为什么 orjson 能这么快
这不是什么魔法,orjson 底层是 Rust 写的,直接编译成机器码,走的是零拷贝 + 内存预分配那套。标准库 json 是纯 Python 实现的,每处理一个字段都要经历解释器的调度。说白了,一个是老牛拉破车,一个是踩油门的跑车。
跟 msgspec 比,orjson 平时稳赢;但 msgspec 强在支持 Struct 类型标注,如果你用它的 typed API,某些场景能反超。这块看你的场景。简单粗暴选型:无脑换 orjson,除非你有强类型校验的刚需。
orjson 的三个坑,不先知道会崩
话是这么说,orjson 也不是躺下就能用。我切换的时候踩了三个坑,写出来你绕道走。
- 返回 bytes 而不是 str:orjson 的
dumps直接返回bytes。直接塞进 FastAPI 的 response 会炸,要先.decode()或者告诉 FastAPI 用Response(content=orjson.dumps(data), media_type="application/json")。 - datetime 默认不支持:标准库能直接序列化
datetime对象,orjson 不行。给你抛TypeError。要么传给orjson.dumps(data)之前先转成字符串,要么用opt=orjson.OPT_NAIVE_UTC这种 option 配合default=处理。 - 非字符串 dict key 会报错:序列化
{1: "a"}这种整数 key 的 dict,标准库会帮你转成字符串,orjson 直接拒绝。数据源里如果有整数 key,得先规整。
import orjson
# 一个"正确姿势"的封装
def dumps_orjson(data) -> str:
return orjson.dumps(
data,
option=orjson.OPT_NAIVE_UTC | orjson.OPT_STRICT_INTEGER,
).decode("utf-8")
实用替换:怎么无痛切换
如果你用的是 FastAPI,直接把接口返回的类型定成 Response,把 orjson 的产物塞进去就行。改动几乎只有一行,收益直接拉满。
from fastapi import FastAPI, Response
import orjson
app = FastAPI()
@app.get("/api/export")
def export():
data = fetch_10000_records() # 你原本的查询逻辑
return Response(
content=orjson.dumps(data),
media_type="application/json"
)
我当时的接口从 180ms 降到 60ms,就是因为把序列化这一环从 16ms 干到了 1ms——虽然不至于逆天,但在高并发下,这个差距直接决定了你需要 3 台机器还是 1 台机器。
FAQ
Q: orjson 一定比 msgspec 快吗?
不一定。默认场景 orjson 更快,但 msgspec 用 Struct 做带类型校验的编码时,某些结构会反超。追求极致就 orjson,需要强类型就 msgspec。
Q: 换 orjson 需要改所有代码吗?
不用。封装一个统一的 dumps_json 函数,内部换成 orjson 就行。但要注意 datetime、整数 key 这两个坑,最好统一在封装层处理。
Q: 输出体积会不会变大?
实测体积几乎一样。1 万条记录的标准库产物 1,175,584 字节,orjson 是 1,115,585 字节,反而还小一点。速度才是 orjson 的主场,体积不用担心。
总结
JSON 序列化看着不起眼,但在大数据量接口里就是那个”隐形杀手”。测完我最大的感触是:很多接口慢,不是数据库慢、不是机器弱,而是你用了最慢的那把工具。
这次我踩的坑和优化思路记录下来了。如果你也在被接口性能折磨,不妨先看看这几篇我写过的实战复盘:
- FastAPI 性能调优实战:从 1000 到 15000 req/s
- Python 性能翻车现场:10 个你以为很快但实际很慢的编码模式
- Python 性能剖析三件套:py-spy、Scalene、memray 实战对比
- Python 字符串与正则性能深度实战:7 组实测数据
记住一句话:先量,再换,别瞎调。先找到真正的瓶颈,再对症下药,这才是本神最推荐的姿势。
