Plate 35
destr vs superjson: 665 B vs 4.1 KB Gzip
Aditya Challa5 min read
I bundled JSON helpers on this box: destr 2.0.5 came out at 665 bytes gzip, while superjson 2.2.6 came out at 4,100 bytes. On plain objects, JSON.parse was fastest (~0.44 µs); destr sat near ~0.58 µs. Only superjson round-tripped Date, Set, undefined, and NaN.
Short answer: pick destr when you want a safer drop-in for JSON.parse on possibly-not-JSON strings. Pick superjson when the wire format must revive rich types. Do not treat them as the same tool. No affiliate links in this post.
What I tested
- Machine: 8 vCPU Intel Xeon / 15 GB RAM shared Linux cloud box, tested 6 Oct 2026 about 02:20 IST.
- Versions: destr 2.0.5, superjson 2.2.6, Node 22.20.0 JSON, esbuild 0.28.2 browser ESM minify + gzip -9.
- Inputs: a plain five-field JSON string for parse; a rich object with Date, undefined, NaN, and Set([1]) for SuperJSON stringify/parse. Speed: 100,000 calls, median of 9 rounds, two order-reversed runs. Scripts in /workspace/bench-destr-superjson/.
- Findings: destr 1,305 / 665 B; superjson 11,447 / 4,100 B; JSON re-export 69 / 70 B. Plain parse ~590 / 564 ns (destr), ~589 / 585 (safeDestr), ~441 / 444 (JSON.parse). SuperJSON rich parse ~2,967 / 2,999 ns; rich stringify ~6,787 / 6,765 ns.
- One surprise: SuperJSON.parse on a plain {"a":1} string returned undefined. It expects its own {json, meta} envelope from SuperJSON.stringify. destr("0123") returned the string "0123" while JSON.parse threw.
- Not tested: custom SuperJSON class registries, streaming parsers, MessagePack alternatives, or browser DevTools size after tree-shaking beyond this esbuild cut.
How much smaller is destr?
| Build (esbuild minify, gzip -9) | Minified | gzip |
|---|---|---|
| destr + safeDestr | 1,305 B | 665 B |
| superjson (parse/stringify/serialize exports) | 11,447 B | 4,100 B |
| JSON.parse / JSON.stringify re-export | 69 B | 70 B |
destr is the small safe-parse helper. superjson is a typed transport layer. Native JSON stays free. Measure the delta like size-limit vs bundlesize. Other parse/format swaps in this series: smol-toml vs @iarna/toml and yaml vs js-yaml.
Do the behaviors match?
| Check | destr 2.0.5 | superjson 2.2.6 | JSON |
|---|---|---|---|
| plain {"a":1,"b":"hi"} | {a:1,b:"hi"} | parse → undefined (needs envelope) | {a:1,b:"hi"} |
| string "0123" | "0123" (no throw) | n/a | throws |
| string "undefined" | undefined | n/a | throws |
| Date / Set / undefined / NaN round-trip | no (plain JSON loses them) | yes via meta | Date→ISO string; Set→{}; undefined dropped; NaN→null |
| safeDestr('{bad') | throws SyntaxError | n/a | throws |
destr is tolerant of values that look like JSON but are not. superjson is strict about its envelope and richer about types. Schema validation after parse is a separate layer; see zod vs valibot and ajv vs zod.
Is plain-parse speed a reason to skip destr?
| Cost (100k calls, median of 9, 2 runs, ns/op) | Run 1 | Run 2 |
|---|---|---|
| destr(plain) | 590.2 | 563.5 |
| safeDestr(plain) | 589.4 | 585.1 |
| JSON.parse(plain) | 440.9 | 444.2 |
| SuperJSON.parse(rich envelope) | 2,966.6 | 2,998.8 |
| SuperJSON.stringify(rich) | 6,787.1 | 6,765.4 |
On known-good JSON, JSON.parse won. destr paid a small tax for the safe path. SuperJSON's cost is in a different job: carrying types across the wire. Deep clone and hash helpers nearby: klona vs structuredClone and ohash vs object-hash. HTTP body clients: ky vs axios.
Which one should you pick?
| Situation | My pick |
|---|---|
| Parsing env vars / loosely typed strings that might not be JSON | destr or safeDestr |
| Known-good JSON from your own API | JSON.parse |
| Need Date, Map, Set, undefined, NaN across RPC | superjson |
| Bundle budget under 1 KB for parse helper | destr |
| Prefer zero deps and you control both ends | JSON + explicit ISO fields |
Bottom line: destr 2.0.5 was 665 B gzip versus 4,100 B for superjson 2.2.6. Plain parse favored JSON.parse; only superjson kept rich types. Who should not add superjson: services that already send ISO strings and do not need Set/Map revival. Cookie and merge helpers next door: cookie-es vs cookie and defu vs lodash.merge.
How this was made: I installed both packages on the ShopperCove box, measured esbuild bundles, timed plain and rich parse/stringify loops in two order-reversed runs, and compared behavior on shared fixtures. The JSON results sit next to the scripts. The write-up was drafted with AI help and checked against that output.
Sources
- https://github.com/unjs/destr
- https://github.com/ravionhq/superjson
- https://www.npmjs.com/package/destr
- https://www.npmjs.com/package/superjson
Related
- https://www.shoppercove.com/blog/size-limit-vs-bundlesize
- https://www.shoppercove.com/blog/smol-toml-vs-iarna-toml
- https://www.shoppercove.com/blog/yaml-vs-js-yaml
- https://www.shoppercove.com/blog/zod-vs-valibot
- https://www.shoppercove.com/blog/ajv-vs-zod
- https://www.shoppercove.com/blog/klona-vs-structuredclone
- https://www.shoppercove.com/blog/ohash-vs-object-hash
- https://www.shoppercove.com/blog/ky-vs-axios
Lab evidence
What I found running this
Hands-on on ShopperCove box 6 Oct 2026 ~02:20 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 22.20.0). destr 2.0.5 vs superjson 2.2.6, esbuild 0.28.2 minify + gzip -9. Size: destr 1,305/665 B vs superjson 11,447/4,100 B vs JSON re-export 69/70 B. Speed (100,000 calls, 9-round median, order-reversed, ns/op): destr plain 590.2 / 563.5; safeDestr 589.4 / 585.1; JSON.parse 440.9 / 444.2; SuperJSON rich parse 2,966.6 / 2,998.8; rich stringify 6,787.1 / 6,765.4. Behavior: SuperJSON.parse on plain JSON → undefined; destr tolerates "0123"; rich Date/Set/undefined/NaN only via SuperJSON. Not tested: custom class registries, streaming parsers, MessagePack. No affiliate.