Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
node_modules
dist
coverage
conf/Conf.d.ts
conf/Conf.d.ts
benchmark-results.json
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![AutoRel](https://img.shields.io/badge/%F0%9F%9A%80%20AutoRel-2D4DDE)](https://github.com/mhweiner/autorel)

Lightning-fast, zero-dependency runtime type validation for TS/JS. 25x faster and 15x smaller than zod (4kb vs 60kb gzipped).
Lightning-fast, zero-dependency runtime type validation for TS/JS. ~14× faster and 15× smaller than zod (4kb vs 60kb gzipped).

**🚀 Fast & reliable performance**

- 25x faster than `zod` and `yup`, 4.3x faster than `joi` (see [Performance](#performance) section)
- ~14× faster than `zod`, ~9× faster than `joi`, ~160× faster than `yup` (see [Performance](#performance))
- Supports tree-shaking via ES Modules so you only bundle what you use
- No dependencies
- 100% test coverage
Expand All @@ -30,6 +30,7 @@ Lightning-fast, zero-dependency runtime type validation for TS/JS. 25x faster an
# Table of contents

- [Performance](#performance)
- [Benchmark details](docs/benchmark.md)
- [Example](#example)
- [Taking advantage of tree-shaking](#taking-advantage-of-tree-shaking)
- [Nested objects](#nested-objects)
Expand All @@ -41,16 +42,18 @@ Lightning-fast, zero-dependency runtime type validation for TS/JS. 25x faster an

# Performance

runtyp is designed for speed. Here's how it compares to other popular programmatic validation libraries:
Each library validates the same nested user object **100,000 times with valid input** and **100,000 times with invalid input** (**200,000 runs total**). Lower total time is faster.

| Library | Valid Data | Invalid Data | Total Time | Relative Speed |
|---------|------------|--------------|------------|----------------|
| **runtyp** | 0.0005ms | 0.0009ms | 142.47ms | 1.0x (fastest) |
| **joi** | 0.0042ms | 0.0018ms | 607.70ms | 4.3x slower |
| **yup** | 0.0145ms | 0.0209ms | 3532.78ms | 24.8x slower |
| **zod** | 0.0007ms | 0.0357ms | 3637.74ms | 25.5x slower |
**Versions tested:** runtyp 1.0.0 · joi 18.2.3 · zod 4.4.3 · yup 1.7.1 · Node v24.16.0

*Benchmark results from 100,000 iterations of complex object validation with nested objects, arrays, and various validation rules. Lower times are better.*
| Library | Version | Total time (200k runs) | vs runtyp |
|---------|---------|------------------------|-----------|
| **runtyp** | 1.0.0 | **97 ms** | fastest |
| **joi** | 18.2.3 | 901 ms | 9.3× slower |
| **zod** | 4.4.3 | 1,389 ms | 14.3× slower |
| **yup** | 1.7.1 | 15,837 ms | 163× slower |

Measured 2026-08-02 with pinned versions above. Invalid runs collect all field errors (same depth for every library). **[Full benchmark breakdown →](docs/benchmark.md)** (methodology, per-run timings, test schema, how to reproduce).

# Installation

Expand Down
219 changes: 110 additions & 109 deletions benchmark.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
#!/usr/bin/env node

const {performance} = require('perf_hooks');
const fs = require('fs');
const path = require('path');

// Import validation libraries
const {predicates: p} = require('./dist/index.js');
const zod = require('zod');
const joi = require('joi');
const yup = require('yup');
// Removed AJV - it's a JSON Schema validator, not a programmatic validation library

// Test data
const ITERATIONS = 100_000;

// Test data — same payload for every library
const validUser = {
name: 'John Doe',
email: 'john@example.com',
Expand Down Expand Up @@ -38,7 +41,6 @@ const invalidUser = {
tags: [],
};

// Define schemas for each library
const runtypSchema = p.object({
name: p.string({len: {min: 1, max: 100}}),
email: p.email(),
Expand Down Expand Up @@ -68,153 +70,152 @@ const zodSchema = zod.object({
});

const joiSchema = joi.object({
name: joi.string().min(1).max(100)
.required(),
name: joi.string().min(1).max(100).required(),
email: joi.string().email().required(),
age: joi.number().min(0).max(150)
.required(),
age: joi.number().min(0).max(150).required(),
phone: joi.string().pattern(/^\(\d{3}\) \d{3}-\d{4}$/).required(),
address: joi.object({
street: joi.string().min(1).required(),
city: joi.string().min(1).required(),
state: joi.string().length(2).required(),
zip: joi.string().pattern(/^\d{5}$/).required(),
}).required(),
tags: joi.array().items(joi.string()).min(1)
.required(),
tags: joi.array().items(joi.string()).min(1).required(),
});

const yupSchema = yup.object({
name: yup.string().min(1).max(100)
.required(),
name: yup.string().min(1).max(100).required(),
email: yup.string().email().required(),
age: yup.number().min(0).max(150)
.required(),
age: yup.number().min(0).max(150).required(),
phone: yup.string().matches(/^\(\d{3}\) \d{3}-\d{4}$/).required(),
address: yup.object({
street: yup.string().min(1).required(),
city: yup.string().min(1).required(),
state: yup.string().length(2).required(),
zip: yup.string().matches(/^\d{5}$/).required(),
}).required(),
tags: yup.array().of(yup.string()).min(1)
.required(),
tags: yup.array().of(yup.string()).min(1).required(),
});

// Removed AJV schema definition

// Benchmark function
function benchmark(name, fn, iterations = 100000) {

console.log(`\nBenchmarking ${name}...`);

// Warm up
for (let i = 0; i < 1000; i++) {

fn(validUser);
const libraries = [
{
name: 'runtyp',
version: require('./package.json').version,
validate: (data) => runtypSchema(data).isValid,
},
{
name: 'zod',
version: require('zod/package.json').version,
validate: (data) => zodSchema.safeParse(data).success,
},
{
name: 'joi',
version: require('joi/package.json').version,
validate: (data) => !joiSchema.validate(data, {abortEarly: false}).error,
},
{
name: 'yup',
version: require('yup/package.json').version,
validate: (data) => {
try {
yupSchema.validateSync(data, {abortEarly: false});
return true;
} catch {
return false;
}
},
},
];

function assertFixtures(librariesToCheck) {
for (const {name, validate} of librariesToCheck) {
if (!validate(validUser)) {
throw new Error(`${name}: validUser should pass validation`);
}
if (validate(invalidUser)) {
throw new Error(`${name}: invalidUser should fail validation`);
}
}
}

// Benchmark valid data
const validStart = performance.now();

function timeRuns(fn, data, iterations) {
const start = performance.now();
for (let i = 0; i < iterations; i++) {

fn(validUser);

fn(data);
}
const validEnd = performance.now();
const validTime = validEnd - validStart;

// Benchmark invalid data
const invalidStart = performance.now();

for (let i = 0; i < iterations; i++) {
return performance.now() - start;
}

fn(invalidUser);
function benchmarkLibrary({name, version, validate}, iterations) {
console.log(`\nBenchmarking ${name}@${version}...`);

for (let i = 0; i < 1000; i++) {
validate(validUser);
}
const invalidEnd = performance.now();
const invalidTime = invalidEnd - invalidStart;

const avgValidTime = validTime / iterations;
const avgInvalidTime = invalidTime / iterations;

console.log(` Valid data: ${avgValidTime.toFixed(4)}ms per validation`);
console.log(` Invalid data: ${avgInvalidTime.toFixed(4)}ms per validation`);
console.log(` Total time: ${(validTime + invalidTime).toFixed(2)}ms`);
const validMs = timeRuns(validate, validUser, iterations);
const invalidMs = timeRuns(validate, invalidUser, iterations);
const totalMs = validMs + invalidMs;

return {
const result = {
name,
validTime: avgValidTime,
invalidTime: avgInvalidTime,
totalTime: validTime + invalidTime,
version,
iterations,
validMs,
invalidMs,
totalMs,
avgValidMs: validMs / iterations,
avgInvalidMs: invalidMs / iterations,
};

}

// Validation functions
const runtypValid = (data) => runtypSchema(data);
const zodValid = (data) => {
console.log(` Valid passes: ${validMs.toFixed(2)}ms total (${result.avgValidMs.toFixed(4)}ms / run)`);
console.log(` Invalid passes: ${invalidMs.toFixed(2)}ms total (${result.avgInvalidMs.toFixed(4)}ms / run)`);
console.log(` Combined: ${totalMs.toFixed(2)}ms total (${iterations.toLocaleString()} valid + ${iterations.toLocaleString()} invalid)`);

try {

zodSchema.parse(data);
return true;

} catch {

return false;

}

};
const joiValid = (data) => {

const result = joiSchema.validate(data);

return !result.error;

};
const yupValid = (data) => {

try {

yupSchema.validateSync(data);
return true;
return result;
}

} catch {
console.log('Validation library benchmark');
console.log('============================');
console.log('Versions tested:');
libraries.forEach(({name, version}) => {
console.log(` ${name} ${version}`);
});
console.log(`Node ${process.version}`);
console.log('');
console.log(`Each library validates the same user object ${ITERATIONS.toLocaleString()} times with valid data,`);
console.log(`then ${ITERATIONS.toLocaleString()} times with invalid data (${(ITERATIONS * 2).toLocaleString()} runs total).`);
console.log('Invalid runs collect all field errors (abortEarly: false for Joi/Yup).');
console.log('Lower total time is faster. See docs/benchmark.md for methodology and per-run breakdown.');

return false;
assertFixtures(libraries);
console.log('\nFixture check passed: validUser passes and invalidUser fails for every library.');

}
const results = libraries.map((lib) => benchmarkLibrary(lib, ITERATIONS));
results.sort((a, b) => a.totalMs - b.totalMs);

};
// Removed AJV validation function

// Run benchmarks
console.log('🚀 Validation Library Performance Benchmark');
console.log('==========================================');
console.log('Testing with 100,000 iterations each...');

const results = [
benchmark('runtyp', runtypValid),
benchmark('zod', zodValid),
benchmark('joi', joiValid),
benchmark('yup', yupValid),
];
const fastest = results[0].totalMs;

// Sort by total time (fastest first)
results.sort((a, b) => a.totalTime - b.totalTime);

console.log('\n📊 Results (fastest to slowest):');
console.log('================================');
console.log('\nResults (fastest to slowest, by combined total time):');
console.log('=====================================================');
results.forEach((result, index) => {
const ratio = result.totalMs / fastest;
const suffix = index === 0 ? '(fastest)' : `(${ratio.toFixed(1)}x slower)`;
console.log(`${index + 1}. ${result.name}@${result.version}: ${result.totalMs.toFixed(2)}ms ${suffix}`);
});

const speedup = results[0].totalTime / result.totalTime;

console.log(`${index + 1}. ${result.name}: ${result.totalTime.toFixed(2)}ms ${speedup > 1 ? `(${speedup.toFixed(1)}x slower)` : '(fastest)'}`);
const payload = {
runAt: new Date().toISOString(),
nodeVersion: process.version,
iterations: ITERATIONS,
runsPerLibrary: ITERATIONS * 2,
results,
};

});
fs.writeFileSync(
path.join(__dirname, 'benchmark-results.json'),
`${JSON.stringify(payload, null, 2)}\n`,
);

console.log('\n✅ Benchmark complete!');
console.log('\nWrote benchmark-results.json');
console.log('Benchmark complete.');
Loading
Loading