structuredClone 比 JSON 往返还慢一倍
线上一个订单快照函数,20 万条记录用 JSON.parse(JSON.stringify(x)) 深拷贝,一次 379 ms,高峰期把这个接口压到 4 秒。有人建议换 structuredClone,实测 683 ms,慢了一倍。最后手写逐字段拷贝,16 ms。
实验
20 万个对象,每个 5 个字段(含 1 个字符串数组、1 个嵌套对象),各跑 3 轮取最快:
| 写法 | 耗时 | 复制后堆增量 |
|---|---|---|
| JSON 往返 | 379.5 ms | 38.2 MB |
| structuredClone | 682.8 ms | 51.9 MB |
| 手写逐字段 | 16.3 ms | 30.5 MB |
手写比 JSON 快 23 倍。这不是手写优化得好,是另外两种被迫做通用工作。
为什么 structuredClone 最慢
它走的是结构化克隆算法:每个值都要查类型、按表分派,还必须支持循环引用、Map/Set、ArrayBuffer、Transferable、Blob/File,于是它不能假设「你这就是一个普通对象数组」。每个节点走一套通用分支,还要维护一张 seen 映射表来处理循环引用——这也是它内存多出 12 MB 的原因。
JSON 往返看起来很笨(先变字符串再解析),但 JSON.stringify / JSON.parse 都是原生实现:序列化是顺序写 buffer,解析有专门的快速路径。代价是多付一次编码加一次解码的字节搬运。
手写版本没有分派:字段形状固定,偏移已知,循环体里就是几条赋值。
数值数据反过来
| 数据 | 方式 | 耗时 |
|---|---|---|
| Float64Array 400 万 | structuredClone | 17.8 ms |
| Float64Array 400 万 | slice() | 8.3 ms |
| 普通数组 400 万 | structuredClone | 202.5 ms |
| 普通数组 400 万 | JSON 往返 | 436.3 ms |
typed array 的克隆是整块 memcpy,两种方案都快;普通数组装 400 万个数字,JSON 要写 400 万段十进制文本再逐个解析。同一份数据,容器选错,深拷贝慢 25 倍。
语义:不是所有东西都能用 JSON 拷
| 值类型 | JSON 往返 | structuredClone |
|---|---|---|
| Date | 变成字符串 | 保留 Date |
undefined 字段 |
整个字段消失 | 保留 |
| Map / Set | 变成空对象 | 保留 |
| BigInt | 抛 TypeError | 保留 |
| 类实例 | 丢原型 | 丢原型 |
| 循环引用 | 抛 TypeError | 支持 |
| 函数 / Symbol | 静默丢弃 | 抛 DataCloneError |
注意最后两行:structuredClone 不是什么都拷得动,函数直接抛错;JSON 是悄悄扔掉。两种失败方式各有坑——丢字段让下游拿到 undefined,抛错至少当场炸。
两者都不保留原型,克隆后 instanceof 一律为 false。要保留类型只能手写,或者自己写工厂函数。
结论
- 拷贝固定形状的业务对象,手写逐字段。20 万条 16 ms 与 379 ms 是数量级差别。
- 需要通用深拷贝、数据里有 Date/Map/循环引用,用
structuredClone,接受它慢,别放在热路径上。 - 只做传输、能接受类型丢失,JSON 往返仍是兼容性最好的选择。
- 大块数字用
Float64Array,克隆就是 memcpy,slice()还比structuredClone快一倍。 - 判断标准不是哪个 API 更现代,而是要拷的是不是同一形状的对象。
数据来自 Node 24.22.1 / Windows,--expose-gc --max-old-space-size=8192。
- 点赞
- 收藏
- 关注作者
评论(0)