Skip to content

Benchmarks ​

Every number on this page comes from scripts in the repository, recorded in bench/results/ 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 orders of magnitude carry over to other hardware, the exact figures do not. How the core gets its speed is on the Performance page.

Compared with other libraries ​

The object of the typescript-runtime-type-benchmarks suite, flat (6 fields) and nested (6 + 3), in the suite's four modes, plus two scenarios the suite does not have: an invalid value with four wrong fields, where every library is asked for every issue with its path, and building the schema. Each library is used the way its documentation shows (bench/compare/adapters/). Pick a scenario and a mode; hover over or focus a bar for the time of one call, the spread of the samples and the exact version.

Nested object, parseStrict
A valid object of 6 fields and a nested object of 3. parseStrict: unknown keys rejected, the value returned. ops/s, log scale, higher is better.
TypeBoxCompile(…).Check()
43.2M
metaschemacheck() · compile(), JIT
29.9M
Ajvcompile(…)(value), draft 2020-12
23.5M
ZodsafeParse()
7.34M
metaschemacheck() · closures
4.81M
ArkTypetype(value) · allows()
4.02M
ValibotsafeParse() · is()
1.8M
Effect SchemadecodeUnknownResult() · is()
1M
Metarhia metaschemacheck() · strict only
550k
Joivalidate(), convert: false
543k
Superstructvalidate() · is()
266k
YupvalidateSync() · isValidSync(), strict
175k
TypeBoxValue.Check()
118k
Apple M3 Max · 16 cores · Node v24.14.1 · median of 5 × 1 s samples, each library in a fresh process · 2026-10-13
LibraryFlat object, parseSafe
ops/s
Flat object, parseStrict
ops/s
Flat object, assertLoose
ops/s
Flat object, assertStrict
ops/s
Nested object, parseSafe
ops/s
Nested object, parseStrict
ops/s
Nested object, assertLoose
ops/s
Nested object, assertStrict
ops/s
Every issue of an invalid value
ops/s
Build a schema
schemas/s
metaschema
check() · compile(), JIT · v3.3.0
48,400,00038,900,00049,700,00040,800,00042,000,00029,900,00044,100,00030,300,0006,650,00032,300
metaschema
check() · closures · v3.3.0
9,430,0009,020,00010,000,0009,200,0005,100,0004,810,0005,090,0004,750,0003,000,000190,000
Metarhia metaschema
check() · strict only · v2.2.2
—958,000—959,000—550,000—546,000428,000282,000
Zod
safeParse() · v4.6.6
26,500,00012,800,00027,800,00012,700,00016,700,0007,340,00016,900,0007,160,000445,00082,100
Valibot
safeParse() · is() · v1.5.0
3,570,0002,950,0003,620,0003,010,0002,130,0001,800,0002,080,0001,780,0001,150,0002,050,000
ArkType
type(value) · allows() · v2.2.8
50,500,0007,400,000171,000,0008,640,00038,100,0004,020,000134,000,0004,430,00068,00014,600
Effect Schema
decodeUnknownResult() · is() · v4.0.3
4,200,0001,970,0004,220,0001,980,0002,310,0001,000,0002,310,0001,000,000359,000672,000
TypeBox
Compile(…).Check() · v1.3.36
165,000,00079,900,000188,000,00083,700,000119,000,00043,200,000136,000,00043,800,00090,60036,200
TypeBox
Value.Check() · v1.3.36
263,000191,000264,000192,000170,000118,000167,000114,00089,400422,000
Ajv
compile(…)(value), draft 2020-12 · v8.20.0
65,700,00034,600,00071,900,00035,300,00051,500,00023,500,00055,400,00024,100,00015,000,0001,840
Yup
validateSync() · isValidSync(), strict · v1.7.1
313,000298,000314,000295,000184,000175,000185,000168,00011,80053,900
Joi
validate(), convert: false · v18.2.9
851,000843,000834,000855,000534,000543,000535,000525,000257,00037,400
Superstruct
validate() · is() · v2.0.2
492,000453,000496,000443,000304,000266,000299,000257,000100,0001,770,000

Apple M3 Max · 16 cores · Node v24.14.1 · median of 5 × 1 s samples, each library in a fresh process · 2026-10-13. Medians, rounded to three significant figures; — where the library cannot express the mode.

How to read it ​

The libraries do different work for one call, so read the chart as a price list, not a ranking.

  • A verdict and a result are different prices. TypeBox's Compile, Ajv and ArkType's allows return a boolean from generated code and allocate nothing on a valid value: that is the floor of a few nanoseconds a call, and the reason they lead the loose modes. metaschema's check returns a ValidationResult with a context behind it (two allocations a call); Zod, Valibot and Effect build a result object and, in the parse modes, a copy of the value without its unknown keys.
  • The strict modes cost every code generator the same scan. Rejecting unknown keys means counting the keys of the value, which is where the generated validators come closest to one another, and where metaschema's JIT sits among them.
  • Generated code or not. metaschema's JIT, TypeBox's Compile, Ajv, ArkType and Zod's object fast path generate code with new Function. Where that is forbidden (a strict Content Security Policy, Cloudflare Workers) metaschema keeps its closures, the second metaschema row: still ahead of every interpreter in the set (Valibot, Effect Schema, TypeBox's Value.Check, Joi, Superstruct, Yup and Metarhia's metaschema). Zod and ArkType fall back to interpreters of their own there, which this run does not measure.
  • Every issue is where the work differs most. Asked for every issue of an invalid value with its path, a library that throws, formats messages or builds an issue tree per call pays for it; metaschema records issues as data and renders each message once, after the walk. Ajv's generated code builds its error objects inline and is the one faster here, with a JSON Pointer string for a path; Ajv's own security guide advises against allErrors in production.
  • Building a schema is the other side of code generation. A library that generates code pays for it once per schema: Ajv builds about two thousand schemas a second, ArkType, TypeBox's Compile and metaschema's JIT a few tens of thousands, metaschema's closures two hundred thousand, and Valibot and Superstruct, whose schemas are plain objects of functions, two million. It matters at start-up and for a program that builds schemas per request.
  • Metarhia's metaschema has no loose mode (its check always rejects unknown keys), so it is missing from those charts. TypeBox 1.x's Value.Check is an interpreter of JSON Schema and much slower than its Compile, and slower than 0.34's was. Yup runs with strict: true and Joi with convert: false, so that neither casts; Joi also needs unsafe() on the number fields to accept Number.MAX_VALUE.

Method ​

  • Every (library, scenario) pair runs in a fresh process, so no library's JIT feedback or heap state leaks into the next one.
  • Before anything is timed, each adapter must give the scenario's verdicts: the valid value accepted, the value with unknown keys accepted or rejected as the mode says, a value with a wrong field rejected, the four issues of the invalid value at their four paths, a built schema that accepts the valid value and rejects the invalid one. A mismatch aborts the run: a check that fails at once is fast and measures nothing.
  • The measured function is called through one megamorphic call site, the same for every library, so no library's check is inlined into the measuring loop; that is how a program calls a validator. Calls are timed in chunks, so reading the clock never dominates a call of a few nanoseconds.
  • After a one-second warmup, calls are timed in five one-second samples and the median is reported; the tooltip shows the spread.
  • The suite's own published numbers come from a different harness (benchmark.js, direct calls in one process), so they are not comparable with these one to one, only the order is.

Cold start ​

Loading the library, building the strict nested schema and checking one value, in a fresh process: what a command-line tool, a serverless function or a test file pays once.

Cold start
Load the library, build the strict nested schema, check one value: a fresh process, median of 20 runs. Lower is better.
Superstructvalidate() · is()
1.2 ms
ValibotsafeParse() · is()
3.9 ms
YupvalidateSync() · isValidSync(), strict
4.2 ms
metaschemacheck() · closures
6.1 ms
metaschemacheck() · compile(), JIT
7.8 ms
Metarhia metaschemacheck() · strict only
21 ms
Joivalidate(), convert: false
25 ms
ZodsafeParse()
30 ms
Ajvcompile(…)(value), draft 2020-12
39 ms
Effect SchemadecodeUnknownResult() · is()
73 ms
TypeBoxValue.Check()
98 ms
TypeBoxCompile(…).Check()
99 ms
ArkTypetype(value) · allows()
109 ms
Apple M3 Max · 16 cores · Node v24.14.1
LibraryLoadBuildFirst checkTotal
Superstruct
validate() · is() · v2.0.2
0.7 ms0.2 ms0.3 ms1.2 ms
Valibot
safeParse() · is() · v1.5.0
3.7 ms0.1 ms0.1 ms3.9 ms
Yup
validateSync() · isValidSync(), strict · v1.7.1
2.4 ms1.1 ms0.7 ms4.2 ms
metaschema
check() · closures · v3.3.0
4.7 ms1.0 ms0.4 ms6.1 ms
metaschema
check() · compile(), JIT · v3.3.0
5.9 ms1.7 ms0.2 ms7.8 ms
Metarhia metaschema
check() · strict only · v2.2.2
20.7 ms0.6 ms0.2 ms21.5 ms
Joi
validate(), convert: false · v18.2.9
23.1 ms1.2 ms0.6 ms25.0 ms
Zod
safeParse() · v4.6.6
27.7 ms1.5 ms1.2 ms30.3 ms
Ajv
compile(…)(value), draft 2020-12 · v8.20.0
14.6 ms24.4 ms0.2 ms39.3 ms
Effect Schema
decodeUnknownResult() · is() · v4.0.3
72.4 ms0.2 ms0.8 ms73.4 ms
TypeBox
Value.Check() · v1.3.36
96.6 ms0.1 ms1.1 ms97.8 ms
TypeBox
Compile(…).Check() · v1.3.36
96.9 ms1.9 ms0.0 ms98.9 ms
ArkType
type(value) · allows() · v2.2.8
107.8 ms1.4 ms0.1 ms109.3 ms

Bundle size ​

What a browser program pays to validate the comparison's object: an entry that imports what it uses, builds the strict nested schema and exports a check, bundled with esbuild (minified, tree-shaken) for the browser. Measuring a whole package instead would charge a modular library for the functions a program never imports.

Bundle size
A browser bundle that builds the strict nested schema and checks a value (esbuild, minified, tree-shaken), min+gzip. Lower is better.
ValibotsafeParse() · is()
1.2 KB
Superstructvalidate() · is()
1.5 KB
Metarhia metaschemacheck() · strict only
11.0 KB
YupvalidateSync() · isValidSync(), strict
13.2 KB
metaschemacheck() · closures
18.9 KB
metaschemacheck() · compile(), JIT
23.1 KB
Effect SchemadecodeUnknownResult() · is()
24.4 KB
ZodsafeParse()
25.7 KB
TypeBoxValue.Check()
37.5 KB
Ajvcompile(…)(value), draft 2020-12
39.5 KB
TypeBoxCompile(…).Check()
40.8 KB
ArkTypetype(value) · allows()
46.4 KB
Joivalidate(), convert: false
53.3 KB
Libraryminmin+gzip
Valibot
safeParse() · is() · v1.5.0
3.3 KB1.2 KB
Superstruct
validate() · is() · v2.0.2
3.5 KB1.5 KB
Metarhia metaschema
check() · strict only · v2.2.2
30.0 KB11.0 KBminified with keepNames: its checks compare constructor names
Yup
validateSync() · isValidSync(), strict · v1.7.1
41.4 KB13.2 KB
metaschema
check() · closures · v3.3.0
52.6 KB18.9 KB
metaschema
check() · compile(), JIT · v3.3.0
65.2 KB23.1 KB
Effect Schema
decodeUnknownResult() · is() · v4.0.3
72.9 KB24.4 KB
Zod
safeParse() · v4.6.6
88.4 KB25.7 KB
TypeBox
Value.Check() · v1.3.36
146.7 KB37.5 KB
Ajv
compile(…)(value), draft 2020-12 · v8.20.0
130.9 KB39.5 KB
TypeBox
Compile(…).Check() · v1.3.36
161.5 KB40.8 KB
ArkType
type(value) · allows() · v2.2.8
151.9 KB46.4 KB
Joi
validate(), convert: false · v18.2.9
169.5 KB53.3 KB

Valibot and Superstruct are small because a program imports only the functions it uses (Zod's own zod/mini builds the same schema in about 5 KB). metaschema's core is one entry: the whole language, with unions, references, the format rule, parse and the async methods, comes with the first import, and the JIT adds 4.2 KB to it. Metarhia's metaschema is smaller and validates less: no unions, no parse, no async validators, issues as strings.

Release over release ​

metaschema's own scenarios (a flat struct of four fields and a nested struct with collections and a tuple, valid and invalid, and Schema.from of both) on Metarhia's metaschema 2.2.2, the project this one was forked from, and on every release since, measured on one machine with today's harness (bench/releases.js loads a checkout of each release).

metaschema 3.3.0 against Metarhia's metaschema 2.2.2
How many times faster, scenario by scenario (1 = as fast). Higher is better.
Closures, every schema
Schema.from — flat struct581k → 332k schemas/s
×0.6
Schema.from — nested struct156k → 91.1k schemas/s
×0.6
check — flat struct, valid1.44M → 11.6M ops/s
×8.0
check — flat struct, invalid1.01M → 3.85M ops/s
×3.8
check — nested struct, valid238k → 2.91M ops/s
×12
check — nested struct, invalid222k → 1.59M ops/s
×7.2
JIT, after compile()
check — flat struct, valid1.44M → 38.2M ops/s
×26
check — flat struct, invalid1.01M → 6.24M ops/s
×6.2
check — nested struct, valid238k → 19.7M ops/s
×83
check — nested struct, invalid222k → 2.12M ops/s
×9.5
Apple M3 Max · 16 cores · Node v24.14.1 · median of 5 × 1 s samples, each release in a fresh process · 2026-10-13
check — nested struct, valid, release over release
ops/s, linear scale, higher is better. The same harness and machine for every release.
Closures, every schema
Metarhia 2.2.2Metarhia metaschema
238k
1.0.0×1.5 vs Metarhia 2.2.2
368k
2.0.0×12 vs Metarhia 2.2.2
2.95M
3.0.0×12 vs Metarhia 2.2.2
2.93M
3.1.0×12 vs Metarhia 2.2.2
2.92M
3.2.0×12 vs Metarhia 2.2.2
2.91M
3.3.0×12 vs Metarhia 2.2.2
2.91M
JIT, after compile()
3.0.0×83 vs Metarhia 2.2.2
19.7M
3.1.0×82 vs Metarhia 2.2.2
19.6M
3.2.0×83 vs Metarhia 2.2.2
19.7M
3.3.0×83 vs Metarhia 2.2.2
19.7M
Apple M3 Max · 16 cores · Node v24.14.1 · median of 5 × 1 s samples, each release in a fresh process · 2026-10-13
ScenarioMetarhia 2.2.21.0.02.0.03.0.03.0.0
JIT
3.1.03.1.0
JIT
3.2.03.2.0
JIT
3.3.03.3.0
JIT
Schema.from — flat struct581,000321,000362,000372,000—367,000—356,000—332,000—
Schema.from — nested struct156,00095,600104,00099,000—98,400—98,600—91,100—
check — flat struct, valid1,440,0002,180,00010,800,00010,700,00038,600,00011,700,00037,900,00011,600,00037,900,00011,600,00038,200,000
check — flat struct, invalid1,010,0001,110,0004,020,0003,940,0006,130,0003,880,0006,170,0003,900,0006,160,0003,850,0006,240,000
check — nested struct, valid238,000368,0002,950,0002,930,00019,700,0002,920,00019,600,0002,910,00019,700,0002,910,00019,700,000
check — nested struct, invalid222,000300,0001,570,0001,580,0002,140,0001,570,0002,110,0001,570,0002,110,0001,590,0002,120,000

Apple M3 Max · 16 cores · Node v24.14.1 · median of 5 × 1 s samples, each release in a fresh process · 2026-10-13. ops/s (schemas/s for Schema.from), medians.

2.0 rewrote the core (a plan per struct, a closure per field, one context per check), which is most of the distance from Metarhia's; 3.0 added the JIT backend; 3.1 and 3.2 kept the speed of check while adding aliases, formats and the record tools. Schema.from is the one row where metaschema is slower than Metarhia's: a definition is checked when the schema is built (the rules a type accepts, the bounds, the patterns, the plan), so that check never has to.

Reproducing the numbers ​

bash
pnpm bench:compare                      # libraries and cold start → bench/results/compare.json
pnpm size:compare --json                # bundle sizes → bench/results/size.json
git archive v3.1.0 | tar -x -C ../metaschema-3.1.0
node bench/releases.js --src ../metaschema-3.1.0   # one release → bench/results/releases.json
node bench/releases.js --src node_modules/metaschema
node bench/releases.js                  # the current tree, both backends
pnpm bench --save                       # metaschema's own scenarios → bench/baseline.json
pnpm bench:report                       # the charts (docs/public/benchmarks) and the tables here and in the README

pnpm bench:compare --only metaschema,zod --scenarios nested,invalid measures a subset and merges it into the existing results; --samples, --sample-ms and --warmup-ms change the method, --cold-runs 0 skips the cold start.

Released under the MIT License.