Footprint
Bundle size
Section titled “Bundle size”These results measure an esbuild-minified browser bundle for each imported codec API. Validation libraries are excluded from every row.
| Codec | Minified | Gzip |
|---|---|---|
| shorn core + Standard adapter | 17.57 KB | 5.39 KB |
| @msgpack/msgpack | 21.20 KB | 5.93 KB |
| msgpackr | 27.59 KB | 10.39 KB |
| cbor-x | 29.10 KB | 10.82 KB |
| protobufjs/light | 88.35 KB | 25.93 KB |
shorn is the smallest codec measured, but the gzip margin over @msgpack/msgpack is now 9%, not the 27% earlier revisions reported. Size is a lead worth defending, not a settled one. avsc needs a browser stream polyfill, and SchemaPack needs a buffer polyfill, so the harness does not report a zero-polyfill result for either. The shorn npm tarball is 41,432 bytes, of which the source map is roughly two thirds; it is published for debugging and never reaches a bundle.
It tree-shakes per feature
Section titled “It tree-shakes per feature”| import set | minified | gzip | this row adds |
|---|---|---|---|
codec | 17,389 | 5,383 | — |
+ m | 17,969 | 5,523 | 140 gzip |
+ safeEncode / safeDecode | 18,257 | 5,594 | 71 gzip |
+ encodeAsync / decodeAsync | 18,570 | 5,666 | 72 gzip |
+ fingerprinted | 19,768 | 6,061 | 395 gzip |
| everything | 20,268 | 6,232 | 171 gzip |
Only users who import a feature pay for it. Fingerprinting is the most expensive single import at 395 gzip bytes, and a bundle that never calls fingerprinted() never carries it.
Two structural decisions keep that true:
- Async validation is a free function, not a
Schemamethod, because class methods do not tree-shake — every method onSchemais retained by every bundle that touches a codec. See ADR 0001. - The Standard Schema compiler constructs wire schemas directly rather than through
m. Referencingmpinned the whole builder object into every bundle, includingbytesandfloat32, which no JSON Schema type can ever select. Removing that reference took 607 minified and 145 gzip bytes off acodec-only import.
The one place tree-shaking stops is m itself: it is a single object, so import { m } retains all twelve builders whether you call two or twelve. That is a deliberate trade — see ADR 0004.
Add your validator on top. shorn does not ship one, which is the point — you were already paying for Zod.
Cold setup
Section titled “Cold setup”Schema and codec construction plus the first Person encode.
| Codec | Cold setup |
|---|---|
| JSON | 0.08 µs |
| msgpackr records | 1.00 µs |
| SchemaPack | 3.00 µs |
| shorn + Zod | 52–66 µs |
| Avro / avsc | 68.99 µs |
| Protobuf.js reflection | 187.75 µs |
shorn starts faster than Avro but slower than SchemaPack. Most of shorn’s time is Zod schema construction, which applications using Zod already pay. The cost is usually negligible in a long-lived server but can matter in a serverless function that handles one request. Define schemas at module scope so warm invocations reuse them.
Memory
Section titled “Memory”Steady-state retained memory after repeated forced GC in isolated processes, for 100,000 decoded events. Does not measure transient peak allocation.
| Codec | Payload | Encode retained | Decoded value | RSS increase |
|---|---|---|---|---|
| shorn | 4.04 MiB | 4.11 MiB | 36.66 MiB | 72.80 MiB |
| Avro | 4.14 MiB | 4.20 MiB | 33.64 MiB | 61.22 MiB |
| SchemaPack | 4.13 MiB | 4.17 MiB | 47.82 MiB | 64.17 MiB |
| msgpackr records | 4.76 MiB | 17.05 MiB | 32.44 MiB | 62.20 MiB |
| JSON | 15.58 MiB | 15.58 MiB | 24.03 MiB | 88.69 MiB |
Encoding a 4.04 MiB payload retains 4.11 MiB. This is down from 12.10 MiB before encoded output became an exact-size copy. Retaining the result no longer retains a larger backing buffer, and internal buffers larger than 64 KiB are released. These rules also make it safe to reuse one pooled Writer.
Decoded memory is middle of pack, RSS below JSON but above Avro and msgpackr records.
Runtime portability
Section titled “Runtime portability”shorn targets es2022 with esbuild’s neutral platform setting. It is ESM-only and uses no Node built-ins, so it runs in Node 22+, Bun, Deno, browsers, and workers. Nothing in the library needs Node 22 specifically — the floor is set by what CI can prove, and the pinned pnpm no longer runs on Node 20.
The full comparison and a smoke test also ran under Bun 1.3.14. Rankings changed between runtimes, so publish runtime-specific numbers with their runtime and version. There is no browser execution matrix yet — only bundling is measured; see Hostile Input for every unproven claim.
Reproducing
Section titled “Reproducing”pnpm bench:bundle # bundle sizes per import setpnpm bench:startup # cold setuppnpm bench:memory # retained memory in isolated processes