Skip to content

Benchmarks ​

Every number on this page comes from scripts in the repository, recorded in bench/results/ and bench/baseline.json, and turned into these charts and tables by pnpm bench:report. They were measured on one machine, named under each chart. Read them as relative guidance: the order and the ratios carry over to other hardware, the exact figures do not.

Compared with other libraries ​

pnpm bench:compare runs seven payloads through protoarray, JSON and eleven JavaScript codecs: the Protocol Buffers of protobufjs and protobuf-es, the Avro of avsc, the MessagePack of msgpackr and @msgpack/msgpack, the CBOR of cbor-x, MongoDB's BSON, Node's v8.serialize, the typed JSON of superjson and devalue, and Fastify's fast-json-stringify. Each library is used the way its documentation shows (bench/compare/adapters.js): the schema libraries get a schema of the same shape, a union is a protobuf oneof, an Avro union of records and protoarray's discriminated union. Every (library, scenario) pair runs in a fresh process, and an adapter has to give the scenario's value back through its own format before it is timed.

Speed is end to end: an object to text or bytes, and the text or bytes back to an object. A round trip is one of each. Pick a scenario and a metric; hover over or focus a bar for the time of one call, the ratio to JSON, the spread of the rounds and the exact version.

One record: round trip
One record: 10 fields, nested address, tags, 2 orders. Encode one payload and decode it, payloads a second, linear scale, higher is better. The line marks JSON.
protoarraycodegen
940k
msgpackrrecords, shared structures
916k
protoarraystandalone module, no eval
899k
avscType.forSchema
840k
protobufjsreflection, Root.fromJSON
717k
protoarraycodegen + metaschema check
689k
protoarrayclosures, no eval
623k
fast-json-stringifycompiled stringify, JSON.parse
615k
JSONstringify · parse
572k
msgpackrpack · unpack
442k
cbor-xrecords
358k
@msgpack/msgpackEncoder · Decoder
356k
v8.serializestructured clone, Node only
277k
BSONserialize · deserialize
273k
protobuf-escreate · toBinary · fromBinary
261k
devaluestringify · parse
184k
superjsonstringify · parse
109k
Apple M3 Max · 16 cores · Node v24.14.1 · median of 5 × 400 ms rounds, each library and scenario in a fresh process · 2026-10-16
Every scenario: encode, decode and the payload sizes

One record — one record: 10 fields, nested address, tags, 2 orders

Libraryencode ops/sdecode ops/sbytesgzipbrotli
JSON
stringify · parse · Node 24.14.1
1,393,246 (×1.00)970,015 (×1.00)266212178
protoarray
codegen · v2.0.0
2,066,250 (×1.48)1,723,648 (×1.78)135134110
protoarray
closures, no eval · v2.0.0
1,500,853 (×1.08)1,065,375 (×1.10)135134110
protoarray
standalone module, no eval · v2.0.0
2,038,604 (×1.46)1,606,745 (×1.66)135134110
protoarray
codegen + metaschema check · v2.0.0
1,148,641 (×0.82)1,723,478 (×1.78)135134110
protobufjs
reflection, Root.fromJSON · v8.8.1
1,398,695 (×1.00)1,472,331 (×1.52)115127117
protobuf-es
create · toBinary · fromBinary · v2.16.0
487,218 (×0.35)560,158 (×0.58)115127117
avsc
Type.forSchema · v5.7.9
1,626,234 (×1.17)1,738,513 (×1.79)97111101
msgpackr
pack · unpack · v2.1.0
1,187,227 (×0.85)703,240 (×0.72)206207173
msgpackr
records, shared structures · v2.1.0
1,413,209 (×1.01)2,605,367 (×2.69)108124112
@msgpack/msgpack
Encoder · Decoder · v3.1.3
743,786 (×0.53)682,968 (×0.70)198200166
cbor-x
records · v1.6.6
778,290 (×0.56)664,618 (×0.69)215218187
BSON
serialize · deserialize · v7.3.3
620,067 (×0.45)487,593 (×0.50)298235202
v8.serialize
structured clone, Node only · Node 24.14.1
644,403 (×0.46)485,043 (×0.50)245227192
superjson
stringify · parse · v2.2.6
124,081 (×0.09)869,721 (×0.90)275218186
devalue
stringify · parse · v6.0.3
298,033 (×0.21)482,851 (×0.50)311242209
fast-json-stringify
compiled stringify, JSON.parse · v7.1.0
1,772,179 (×1.27)942,027 (×0.97)266212178

1,000 records — 1000 records of the same shape

Libraryencode ops/sdecode ops/sbytesgzipbrotli
JSON
stringify · parse · Node 24.14.1
1,466 (×1.00)891 (×1.00)271,21533,79329,133
protoarray
codegen · v2.0.0
1,958 (×1.34)1,476 (×1.66)140,73830,66626,946
protoarray
closures, no eval · v2.0.0
1,390 (×0.95)978 (×1.10)140,73830,66626,946
protoarray
standalone module, no eval · v2.0.0
1,992 (×1.36)1,471 (×1.65)140,73830,66626,946
protoarray
codegen + metaschema check · v2.0.0
1,090 (×0.74)1,462 (×1.64)140,73830,66626,946
protoarray
columns layout · v2.0.0
2,059 (×1.40)1,535 (×1.72)138,75827,41221,053
protoarray
columns, delta + intern · v2.0.0
2,245 (×1.53)1,670 (×1.87)114,96524,57419,799
protobufjs
reflection, Root.fromJSON · v8.8.1
1,301 (×0.89)1,403 (×1.57)120,45931,41428,985
protobuf-es
create · toBinary · fromBinary · v2.16.0
617 (×0.42)587 (×0.66)120,45931,41428,985
avsc
Type.forSchema · v5.7.9
1,399 (×0.95)1,793 (×2.01)101,83929,89527,334
msgpackr
pack · unpack · v2.1.0
1,219 (×0.83)726 (×0.81)210,06134,13930,555
msgpackr
records, shared structures · v2.1.0
1,538 (×1.05)2,867 (×3.22)112,27430,95728,370
@msgpack/msgpack
Encoder · Decoder · v3.1.3
781 (×0.53)702 (×0.79)202,06133,89530,367
cbor-x
records · v1.6.6
1,373 (×0.94)2,340 (×2.63)125,05131,58229,016
BSON
serialize · deserialize · v7.3.3
635 (×0.43)471 (×0.53)306,97940,38835,974
v8.serialize
structured clone, Node only · Node 24.14.1
1,234 (×0.84)664 (×0.75)247,28035,07231,298
superjson
stringify · parse · v2.2.6
105 (×0.07)904 (×1.01)271,22433,80629,142
devalue
stringify · parse · v6.0.3
249 (×0.17)549 (×0.62)311,51576,72363,418
fast-json-stringify
compiled stringify, JSON.parse · v7.1.0
1,659 (×1.13)884 (×0.99)271,21533,79329,133

Sparse record — 20 optional fields, 3 present

Libraryencode ops/sdecode ops/sbytesgzipbrotli
JSON
stringify · parse · Node 24.14.1
6,800,381 (×1.00)4,562,955 (×1.00)335336
protoarray
codegen · v2.0.0
7,466,131 (×1.10)7,541,369 (×1.65)234327
protoarray
closures, no eval · v2.0.0
3,029,115 (×0.45)2,382,353 (×0.52)234327
protoarray
standalone module, no eval · v2.0.0
6,712,097 (×0.99)7,171,599 (×1.57)234327
protoarray
codegen + metaschema check · v2.0.0
2,089,158 (×0.31)7,340,173 (×1.61)234327
protobufjs
reflection, Root.fromJSON · v8.8.1
5,568,923 (×0.82)9,670,688 (×2.12)113115
protobuf-es
create · toBinary · fromBinary · v2.16.0
966,102 (×0.14)2,827,757 (×0.62)113115
avsc
Type.forSchema · v5.7.9
1,457,467 (×0.21)3,623,007 (×0.79)283523
msgpackr
pack · unpack · v2.1.0
6,136,300 (×0.90)6,733,737 (×1.48)214125
msgpackr
records, shared structures · v2.1.0
5,958,539 (×0.88)12,178,186 (×2.67)92913
@msgpack/msgpack
Encoder · Decoder · v3.1.3
5,224,674 (×0.77)3,403,714 (×0.75)193923
cbor-x
records · v1.6.6
3,036,913 (×0.45)3,620,365 (×0.79)274731
BSON
serialize · deserialize · v7.3.3
3,279,163 (×0.48)4,089,815 (×0.90)335333
v8.serialize
structured clone, Node only · Node 24.14.1
1,238,524 (×0.18)1,367,302 (×0.30)284832
superjson
stringify · parse · v2.2.6
797,123 (×0.12)3,232,153 (×0.71)426240
devalue
stringify · parse · v6.0.3
1,733,712 (×0.25)2,408,284 (×0.53)416143
fast-json-stringify
compiled stringify, JSON.parse · v7.1.0
14,537,667 (×2.14)4,708,885 (×1.03)335336

Typed record — enum, boolean and date heavy record

Libraryencode ops/sdecode ops/sbytesgzipbrotli
JSON
stringify · parse · Node 24.14.1
821,191 (×1.00)2,898,740 (×1.00)12312094
protoarray
codegen · v2.0.0
4,717,208 (×5.74)4,844,478 (×1.67)364636
protoarray
closures, no eval · v2.0.0
3,514,161 (×4.28)3,118,202 (×1.08)364636
protoarray
standalone module, no eval · v2.0.0
4,666,737 (×5.68)4,696,261 (×1.62)364636
protoarray
codegen + metaschema check · v2.0.0
2,803,310 (×3.41)4,851,985 (×1.67)364636
protobufjs
reflection, Root.fromJSON · v8.8.1
3,837,319 (×4.67)12,143,747 (×4.19)133317
protobuf-es
create · toBinary · fromBinary · v2.16.0
1,272,891 (×1.55)2,952,236 (×1.02)133317
avsc
Type.forSchema · v5.7.9
2,135,877 (×2.60)7,911,258 (×2.73)123216
msgpackr
pack · unpack · v2.1.0
2,770,059 (×3.37)2,776,545 (×0.96)739672
msgpackr
records, shared structures · v2.1.0
3,373,007 (×4.11)5,181,591 (×1.79)244528
@msgpack/msgpack
Encoder · Decoder · v3.1.3
1,155,596 (×1.41)1,540,533 (×0.53)719271
cbor-x
records · v1.6.6
1,768,803 (×2.15)1,529,636 (×0.53)7910080
BSON
serialize · deserialize · v7.3.3
1,871,497 (×2.28)1,338,309 (×0.46)9810482
v8.serialize
structured clone, Node only · Node 24.14.1
1,012,846 (×1.23)1,126,288 (×0.39)8910990
superjson
stringify · parse · v2.2.6
291,201 (×0.35)789,994 (×0.27)179153126
devalue
stringify · parse · v6.0.3
604,182 (×0.74)1,043,080 (×0.36)142137112
fast-json-stringify
compiled stringify, JSON.parse · v7.1.0
5,208,951 (×6.34)2,808,826 (×0.97)11010783

Long strings — long Cyrillic strings

Libraryencode ops/sdecode ops/sbytesgzipbrotli
JSON
stringify · parse · Node 24.14.1
1,557,669 (×1.00)1,335,778 (×1.00)816266229
protoarray
codegen · v2.0.0
1,811,907 (×1.16)1,695,038 (×1.27)785230207
protoarray
closures, no eval · v2.0.0
1,673,013 (×1.07)1,557,716 (×1.17)785230207
protoarray
standalone module, no eval · v2.0.0
1,758,113 (×1.13)1,674,028 (×1.25)785230207
protoarray
codegen + metaschema check · v2.0.0
1,548,540 (×0.99)1,693,591 (×1.27)785230207
protobufjs
reflection, Root.fromJSON · v8.8.1
575,285 (×0.37)507,626 (×0.38)777233211
protobuf-es
create · toBinary · fromBinary · v2.16.0
407,763 (×0.26)427,814 (×0.32)777233211
avsc
Type.forSchema · v5.7.9
491,511 (×0.32)769,652 (×0.58)774231210
msgpackr
pack · unpack · v2.1.0
840,197 (×0.54)212,834 (×0.16)800263234
msgpackr
records, shared structures · v2.1.0
862,533 (×0.55)240,954 (×0.18)775230206
@msgpack/msgpack
Encoder · Decoder · v3.1.3
377,771 (×0.24)690,308 (×0.52)798260233
cbor-x
records · v1.6.6
707,953 (×0.45)241,858 (×0.18)806269241
BSON
serialize · deserialize · v7.3.3
604,607 (×0.39)387,918 (×0.29)840290257
v8.serialize
structured clone, Node only · Node 24.14.1
822,278 (×0.53)1,033,535 (×0.77)863263247
superjson
stringify · parse · v2.2.6
310,472 (×0.20)1,177,765 (×0.88)825275233
devalue
stringify · parse · v6.0.3
547,691 (×0.35)924,009 (×0.69)832284243
fast-json-stringify
compiled stringify, JSON.parse · v7.1.0
1,320,247 (×0.85)1,335,361 (×1.00)816266229

100 wrpc packets — 100 wrpc packets: calls, callbacks and events

Libraryencode ops/sdecode ops/sbytesgzipbrotli
JSON
stringify · parse · Node 24.14.1
51,915 (×1.00)24,070 (×1.00)8,834852630
protoarray
codegen · v2.0.0
66,308 (×1.28)49,455 (×2.05)4,332685501
protoarray
closures, no eval · v2.0.0
48,093 (×0.93)26,769 (×1.11)4,332685501
protoarray
standalone module, no eval · v2.0.0
65,648 (×1.26)47,442 (×1.97)4,332685501
protoarray
codegen + metaschema check · v2.0.0
33,902 (×0.65)49,890 (×2.07)4,332685501
protobufjs
reflection, Root.fromJSON · v8.8.1
24,172 (×0.47)37,015 (×1.54)3,691778609
protobuf-es
create · toBinary · fromBinary · v2.16.0
10,961 (×0.21)15,383 (×0.64)3,691778609
avsc
Type.forSchema · v5.7.9
20,632 (×0.40)46,294 (×1.92)2,833633521
msgpackr
pack · unpack · v2.1.0
31,025 (×0.60)23,611 (×0.98)6,800842640
msgpackr
records, shared structures · v2.1.0
28,725 (×0.55)84,980 (×3.53)3,697725566
@msgpack/msgpack
Encoder · Decoder · v3.1.3
25,155 (×0.48)20,986 (×0.87)6,400804614
cbor-x
records · v1.6.6
23,920 (×0.46)50,532 (×2.10)4,401841683
BSON
serialize · deserialize · v7.3.3
16,962 (×0.33)15,135 (×0.63)9,7421,248929
v8.serialize
structured clone, Node only · Node 24.14.1
42,445 (×0.82)17,679 (×0.73)7,772872693
superjson
stringify · parse · v2.2.6
4,248 (×0.08)24,268 (×1.01)8,843860633
devalue
stringify · parse · v6.0.3
9,382 (×0.18)16,310 (×0.68)8,2021,7211,345

32 doubles — 32 doubles (where binary formats shine)

Libraryencode ops/sdecode ops/sbytesgzipbrotli
JSON
stringify · parse · Node 24.14.1
323,242 (×1.00)483,381 (×1.00)764406392
protoarray
codegen · v2.0.0
406,134 (×1.26)733,235 (×1.52)582309295
protoarray
closures, no eval · v2.0.0
360,928 (×1.12)566,586 (×1.17)582309295
protoarray
standalone module, no eval · v2.0.0
404,218 (×1.25)731,900 (×1.51)582309295
protoarray
codegen + metaschema check · v2.0.0
336,196 (×1.04)737,943 (×1.53)582309295
protobufjs
reflection, Root.fromJSON · v8.8.1
709,089 (×2.19)1,944,328 (×4.02)305328304
protobuf-es
create · toBinary · fromBinary · v2.16.0
252,782 (×0.78)455,256 (×0.94)305328304
avsc
Type.forSchema · v5.7.9
1,937,808 (×5.99)3,115,855 (×6.45)256279260
msgpackr
pack · unpack · v2.1.0
1,100,446 (×3.40)428,775 (×0.89)409429376
msgpackr
records, shared structures · v2.1.0
1,413,446 (×4.37)3,523,365 (×7.29)289312280
@msgpack/msgpack
Encoder · Decoder · v3.1.3
731,077 (×2.26)187,123 (×0.39)409429376
cbor-x
records · v1.6.6
711,125 (×2.20)433,751 (×0.90)416435355
BSON
serialize · deserialize · v7.3.3
688,975 (×2.13)474,833 (×0.98)411391341
v8.serialize
structured clone, Node only · Node 24.14.1
644,279 (×1.99)594,638 (×1.23)443387340
superjson
stringify · parse · v2.2.6
87,844 (×0.27)449,255 (×0.93)773420399
devalue
stringify · parse · v6.0.3
230,286 (×0.71)265,668 (×0.55)853454443
fast-json-stringify
compiled stringify, JSON.parse · v7.1.0
2,243,379 (×6.94)479,670 (×0.99)764406392

Apple M3 Max · 16 cores · Node v24.14.1 · median of 5 × 400 ms rounds, each library and scenario in a fresh process · 2026-10-16. Medians; (×n) against JSON; bytes are the wire size of one payload (binary formats before any text encoding), gzip at level 6, brotli at quality 5. The spread of the rounds is in bench/results/compare.json.

Reading the numbers ​

  • Records, strings and tagged messages are where JSON transports live, and where protoarray is the fastest encoder of the comparison: JSON.stringify is native, the generated code that turns an object into its array adds tens of nanoseconds, and the array is about half the text. The binary codecs written in JavaScript encode strings byte by byte and fall behind.
  • Decoding, protoarray is faster than JSON on every payload and faster than every codec on long strings, where JSON.parse reads UTF-8 natively and the binary codecs decode it in JavaScript. msgpackr with shared structures (a structure id and the values, the binary equivalent of protoarray) decodes the other payloads faster than JSON.parse can read text, and avsc, cbor-x's records and protobufjs are ahead on some shapes (lists of records, enums and booleans). Over a round trip protoarray is first on one record, long strings and tagged messages, second behind msgpackr on lists and sparse records and behind protobufjs on typed ones.
  • Numbers are the honest exception: a double prints as up to 17 characters and parses back through a decimal conversion, where avsc and protobufjs read 8 bytes. Use a binary codec for dense numeric payloads.
  • Validation. The wire is checked on decode and the values given to encode are trusted, so protoarray encodes the way protobufjs and msgpackr do, without checks, and decodes with them. Checking the values is metaschema's: the codegen + metaschema check row runs metaschema's check of the same schema before every encode, which is what validating each outgoing value costs.
  • Without eval. The closures row is the backend protoarray falls back to where new Function is not allowed, at about the speed of JSON. The standalone module row is the same codec written into a module at build time (see Standalone Modules): what a strict CSP or an edge runtime gets instead, at the speed of generated code.
  • Sizes: protoarray's wire is JSON text; the binary formats' bytes are before any base64 a text channel would add. After gzip or brotli the formats are close, and the columns layout is the smallest on lists. With the ids written as differences and the cities and tags interned (columns, delta + intern), the thousand records are another 17% smaller before compression and the smallest after it of any format here; interning the unique strings (names, emails) instead would make the gzip larger, since their table compresses worse than the strings in place.
  • The typed JSON libraries (superjson, devalue) and v8.serialize need no schema: they write the keys and the type of every value, which is what they are for, and pay for it in speed and bytes. fast-json-stringify compiles a JSON Schema into a serializer and is the fastest encoder on sparse and typed records; it decodes with JSON.parse and writes the keys.

Cold start ​

A schema is built once, so the tables above leave it out. A program that starts often (a CLI, a serverless function, a page) pays for loading the library, building its schemas and the first call, which runs before the engine has optimized anything. Each library runs ten times in a fresh process, loading itself, building the schema of one record, encoding it and decoding it once.

Cold start
Load the library, build the schema of one record, encode it and decode it once: a fresh process, median of 10 runs. Lower is better.
JSONstringify · parse
0.1 ms
v8.serializestructured clone, Node only
0.4 ms
protoarraystandalone module, no eval
1.6 ms
@msgpack/msgpackEncoder · Decoder
3.9 ms
devaluestringify · parse
5.9 ms
msgpackrrecords, shared structures
6.4 ms
superjsonstringify · parse
6.4 ms
cbor-xrecords
6.5 ms
BSONserialize · deserialize
6.5 ms
protoarraycodegen, from a snapshot
7.3 ms
avscType.forSchema
9.1 ms
protoarrayclosures, no eval
16 ms
protobufjsreflection, Root.fromJSON
16 ms
protoarraycodegen
16 ms
fast-json-stringifycompiled stringify, JSON.parse
26 ms
protobuf-escreate · toBinary · fromBinary
26 ms
Apple M3 Max · 16 cores · Node v24.14.1 · 2026-10-16
LibraryLoadBuildFirst encodeFirst decodeTotal
JSON
stringify · parse · Node 24.14.1
0.00 ms0.02 ms0.02 ms0.01 ms0.06 ms
v8.serialize
structured clone, Node only · Node 24.14.1
0.27 ms0.01 ms0.04 ms0.04 ms0.36 ms
protoarray
standalone module, no eval · v2.0.0
1.2 ms0.02 ms0.15 ms0.16 ms1.6 ms
@msgpack/msgpack
Encoder · Decoder · v3.1.3
2.9 ms0.10 ms0.37 ms0.57 ms3.9 ms
devalue
stringify · parse · v6.0.3
5.1 ms0.01 ms0.49 ms0.26 ms5.9 ms
msgpackr
records, shared structures · v2.1.0
5.3 ms0.03 ms0.63 ms0.43 ms6.4 ms
superjson
stringify · parse · v2.2.6
5.8 ms0.01 ms0.58 ms0.04 ms6.4 ms
cbor-x
records · v1.6.6
5.3 ms0.03 ms0.64 ms0.47 ms6.5 ms
BSON
serialize · deserialize · v7.3.3
5.4 ms0.02 ms0.51 ms0.60 ms6.5 ms
protoarray
codegen, from a snapshot · v2.0.0
3.9 ms3.0 ms0.14 ms0.15 ms7.3 ms
avsc
Type.forSchema · v5.7.9
8.2 ms0.59 ms0.18 ms0.17 ms9.1 ms
protoarray
closures, no eval · v2.0.0
11.6 ms3.8 ms0.11 ms0.17 ms15.7 ms
protobufjs
reflection, Root.fromJSON · v8.8.1
11.1 ms1.5 ms3.1 ms0.35 ms16.2 ms
protoarray
codegen · v2.0.0
11.7 ms4.5 ms0.14 ms0.16 ms16.5 ms
fast-json-stringify
compiled stringify, JSON.parse · v7.1.0
21.0 ms4.5 ms0.26 ms0.01 ms25.7 ms
protobuf-es
create · toBinary · fromBinary · v2.16.0
24.5 ms0.38 ms0.80 ms0.06 ms25.9 ms

A protoarray schema built from a definition loads metaschema too, which reads the definition first. A client that gets its schemas as snapshots from its server needs protoarray alone (from a snapshot), and a standalone module has its codec written already: loading it is the whole cost.

Bundle size ​

What a browser program pays to send and read that record with each library: an entry that imports what it uses, builds the schema the way the library documents and round-trips the value, bundled by esbuild for the browser (minified, tree-shaken) and checked to give the record back (pnpm size:compare). JSON costs nothing, and v8.serialize is Node only.

Bundle size
A browser bundle that builds the schema of one record, encodes it and decodes it (esbuild, minified, tree-shaken), min+gzip. Lower is better.
devaluestringify · parse
4.0 KB
superjsonstringify · parse
4.0 KB
@msgpack/msgpackEncoder · Decoder
5.8 KB
protoarraystandalone module, no eval
6.9 KB
msgpackrrecords, shared structures
10.3 KB
cbor-xrecords
10.6 KB
protobuf-escreate · toBinary · fromBinary
18.2 KB
protoarraycodegen, from a snapshot
22.0 KB
BSONserialize · deserialize
22.2 KB
protobufjsprotobufjs/light, Root.fromJSON
29.2 KB
avscType.forSchema
34.3 KB
protoarraycodegen, metaschema definition
42.6 KB
fast-json-stringifycompiled stringify, JSON.parse
62.9 KB
Libraryminmin+gzip
devalue
stringify · parse · v6.0.3
10.7 KB4.0 KB
superjson
stringify · parse · v2.2.6
11.1 KB4.0 KB
@msgpack/msgpack
Encoder · Decoder · v3.1.3
20.7 KB5.8 KB
protoarray
standalone module, no eval · v2.0.0
18.3 KB6.9 KBthe module protoarray compile writes for this schema, with its inline runtime
msgpackr
records, shared structures · v2.1.0
27.4 KB10.3 KB
cbor-x
records · v1.6.6
28.6 KB10.6 KB
protobuf-es
create · toBinary · fromBinary · v2.16.0
66.6 KB18.2 KBthe runtime and the descriptor generated code embeds
protoarray
codegen, from a snapshot · v2.0.0
60.9 KB22.0 KBthe schema built from its snapshot: a client without metaschema
BSON
serialize · deserialize · v7.3.3
74.8 KB22.2 KB
protobufjs
protobufjs/light, Root.fromJSON · v8.8.1
105.7 KB29.2 KBreflection; static code from pbjs with protobufjs/minimal is smaller, plus the generated code
avsc
Type.forSchema · v5.7.9
115.4 KB34.3 KBavsc's browser build of types, with the buffer and util polyfills it requires
protoarray
codegen, metaschema definition · v2.0.0
120.0 KB42.6 KBprotoarray and @alexify/metaschema, which reads the definition and validates values
fast-json-stringify
compiled stringify, JSON.parse · v7.1.0
217.4 KB62.9 KBencode only; JSON.parse decodes

Release over release ​

pnpm bench:releases measures the scenarios of the comparison through stringify and parse of every release, on both backends, with today's harness on one machine. 2.0 reads metaschema's definitions instead of its own spelling of them and kept the wire, the canonical form and the speed of 1.x: with generated code every scenario is within 7% of the first release that has it, and typed records encode a fifth faster (the text builder); the closures are within 10%, a sparse record 7–9% slower.

protoarray 2.0.0 against its first releases
How many times as fast, scenario by scenario, against the oldest release that has it (1 = as fast; unions came in 1.1). Higher is better.
encodedecode
Generated code
One recordagainst 1.0.0
×1.00×1.02
1,000 recordsagainst 1.0.0
×1.01×0.98
Sparse recordagainst 1.0.0
×0.98×1.02
Typed recordagainst 1.0.0
×1.21×1.00
Long stringsagainst 1.0.0
×1.01×1.02
100 wrpc packetsagainst 1.1.0
×1.01×1.07
32 doublesagainst 1.0.0
×1.06×1.00
Closures, no eval
One recordagainst 1.0.0
×0.95×1.09
1,000 recordsagainst 1.0.0
×0.98×1.04
Sparse recordagainst 1.0.0
×0.93×0.91
Typed recordagainst 1.0.0
×1.17×1.09
Long stringsagainst 1.0.0
×1.02×1.01
100 wrpc packetsagainst 1.1.0
×1.01×1.03
32 doublesagainst 1.0.0
×1.00×0.99
Apple M3 Max · 16 cores · Node v24.14.1 · median of 5 × 400 ms rounds, each release, backend and scenario in a fresh process · 2026-10-16
One record, release over release
One record: 10 fields, nested address, tags, 2 orders. stringify and parse, ops/s, linear scale, higher is better.
encodedecode
Generated code
1.0.0
2.03M1.65M
1.1.0
2.03M1.64M
2.0.0this release
2.04M1.69M
Closures, no eval
1.0.0
1.55M971k
1.1.0
1.47M1.04M
2.0.0this release
1.47M1.06M
Apple M3 Max · 16 cores · Node v24.14.1 · median of 5 × 400 ms rounds, each release, backend and scenario in a fresh process · 2026-10-16
ReleaseOne record
encode / decode
1,000 records
encode / decode
Sparse record
encode / decode
Typed record
encode / decode
Long strings
encode / decode
100 wrpc packets
encode / decode
32 doubles
encode / decode
Schema.from
schemas/s
1.0.0
codegen
2,030,000 / 1,650,0001,960 / 1,5007,200,000 / 7,340,0003,850,000 / 4,770,0001,790,000 / 1,660,000—403,000 / 734,00036,200
1.1.0
codegen
2,030,000 / 1,640,0001,970 / 1,5007,210,000 / 7,180,0004,880,000 / 4,790,0001,750,000 / 1,670,00066,800 / 47,400409,000 / 731,00031,600
2.0.0
codegen
2,040,000 / 1,690,0001,970 / 1,4807,020,000 / 7,460,0004,670,000 / 4,780,0001,800,000 / 1,700,00067,600 / 50,600427,000 / 731,00014,600
1.0.0
closures
1,550,000 / 971,0001,420 / 9373,180,000 / 2,590,0002,950,000 / 2,850,0001,670,000 / 1,540,000—360,000 / 573,000172,000
1.1.0
closures
1,470,000 / 1,040,0001,380 / 9762,930,000 / 2,460,0003,540,000 / 3,110,0001,680,000 / 1,550,00047,800 / 26,000364,000 / 568,00098,400
2.0.0
closures
1,470,000 / 1,060,0001,390 / 9782,950,000 / 2,360,0003,450,000 / 3,090,0001,690,000 / 1,560,00048,100 / 26,800359,000 / 567,00024,000

Apple M3 Max · 16 cores · Node v24.14.1 · median of 5 × 400 ms rounds, each release, backend and scenario in a fresh process · 2026-10-16. ops/s of stringify / parse, medians; — where the release has no union.

Building a schema is where 2.0 pays for metaschema: Schema.from(definition, { metaschema }) has metaschema read the definition first and protoarray read its snapshot after, about twice the time 1.1 took to parse its own spelling of it with generated code (about 70 µs for the record of the comparison) and four times with the closures, where the build is otherwise cheap. A schema is built once; one built from a snapshot skips metaschema, and a standalone module skips building.

protoarray on its two backends ​

pnpm bench is the engine harness for protoarray alone: both backends and every layout, against JSON.stringify and JSON.parse of the keyed object, with --save and --compare to track a change on one machine. The generated code is ahead of JSON on most shapes, ×1.2 to ×2.2. It is behind on a record of 70 flags, which travels as bitmask words JSON does not have to compute, on decoding bytes, where the base64 has to be canonical, and level with it on encoding a sparse record of 70 fields. The closures stay near JSON on most shapes and fall behind on sparse records, where they fill a decoded object one property at a time.

protoarray on its two backends: encode
pnpm bench: stringify against JSON.stringify of the keyed object, ops/s, log scale, higher is better.
JSON, keyed objectgenerated codeclosures
Flat object4 fields
5.43M6.75M5.64M
Nested objectrecords, a list of orders
1.43M1.97M1.51M
Sparse object3 of 20 fields
6.7M6.77M2.65M
Wide sparse object3 of 70 fields
6.64M6.19M970k
Typed objectenum, flags, date
2.98M4.63M3.13M
Tagged uniona wrpc packet
3.62M5.09M3.96M
Moneydecimals
4.87M9.89M6.09M
Wide flags70 names
5.18M1.33M1.13M
Bytes1 KB, base64
990k1.09M1.04M
pnpm bench, bench/baseline.json · Node v24.14.1 · 2026-10-16

The layouts of a list of a thousand records: rows (an array per record, the default), flat (one array with a fixed stride), columns (an array per field) and columns with the ids as differences and the cities and tags interned. See Collections and Layouts.

Layouts of a list of a thousand records
pnpm bench, generated code: lists a second, linear scale, higher is better.
encodedecode
JSONkeyed objects
1,7131,247
rowsan array of arrays, the default
2,2472,002
flatone array, a fixed stride
2,1131,979
columnsan array per field
2,2171,980
columns, delta + internids as differences, cities and tags interned
2,3472,126
pnpm bench, bench/baseline.json · Node v24.14.1 · 2026-10-16

pnpm bench also measures the text builder against the native stringify path with the text read once, as sending it would (see Runtimes), a resolved schema, an inline standalone module, parseItems against parsing the whole text, and Schema.from with each backend.

Running them ​

bash
pnpm bench:compare --save        # the comparison and the cold start, bench/results/compare.json
pnpm size:compare --save         # bundle sizes, bench/results/size.json
pnpm bench:releases --ref v1.0.0 # a release (a tag), merged into bench/results/releases.json
pnpm bench:releases              # the current tree
pnpm bench --save                # the engine harness, bench/baseline.json
pnpm bench:report                # the charts and tables of this page and the README

Every run is manual and none is part of CI. Run them on an otherwise idle machine: a 5% difference hides in the noise of a busy one. --scenarios and --adapters narrow bench:compare, and a saved run merges into the results, so one library or scenario can be measured again on its own.

Released under the MIT License.