Zod vs Valibot vs ArkType: Bundle Size and Speed, Measured
Every few months another "schema validation library comparison" post shows up with a feature matrix: does it support branded types, does it have .transform(), does it do i18n error messages. Cool, but none of that tells
Every few months another "schema validation library comparison" post shows up with a feature matrix: does it support branded types, does it have .transform(), does it do i18n error messages. Cool, but none of that tells you what happens when your bundle ships to a phone on 3G, or when your API gateway is parsing 50,000 nested payloads a minute.
So I stopped reading feature tables and just measured three things: real bundle cost via esbuild's metafile output, parse throughput on nested schemas via tinybench, and cold-start parse time (relevant if you're running this in a Lambda or edge function). Here's what actually happened.
๐ง The Setup
Three libraries, same logical schema, compiled the same way:
- Zod 3.23 (the incumbent, used in half of every tRPC/tanstack-start repo you'll open)
- Valibot 0.31 (built from day one around tree-shaking)
- ArkType 2.0 (the newest, compiles schemas into actual JS predicate functions)
The test schema is a realistic "user with nested orders" shape, 4 levels deep, mixing primitives, arrays, unions, and optional fields:
typescript
// shared shape, expressed per-library below
type User = {
id: string;
email: string;
age: number;
roles: ("admin" | "user" | "guest")[];
address?: {
street: string;
city: string;
zip: string;
geo?: { lat: number; lng: number };
};
orders: {
id: string;
total: number;
items: { sku: string; qty: number; price: number }[];
}[];
};
Built with esbuild --bundle --minify --format=esm, then gzip'd. Parse benchmarks ran with tinybench on Node 20, 10,000 warmup iterations, 50,000 measured iterations, against a fixed valid payload and a fixed invalid payload (to also measure error-path cost, which people conveniently never benchmark).
๐ฆ Bundle Size: What Actually Ships
This is where the "just pick whatever" advice falls apart immediately.
| Library | Import style | Minified | Gzip |
|---|---|---|---|
| Zod | import { z } from "zod" |
61.2 KB | 15.9 KB |
| Valibot |
import * as v from "valibot" (no tree-shake) |
58.4 KB | 14.7 KB |
| Valibot | tree-shaken, only used validators | 3.1 KB | 1.3 KB |
| ArkType | import { type } from "arktype" |
47.8 KB | 13.2 KB |
Zod doesn't tree-shake meaningfully โ its API surface is built on method chaining off a shared prototype (z.string().email().min(3)), so bundlers can't prove which parts are unused and drop them. You pay for the whole library basically every time, regardless of how small your schema is.
Valibot's pitch is real: it ships as individual functions (object(), string(), minLength()) rather than chained methods, so esbuild/rollup tree-shaking actually works. If your schema only uses six validators, you ship six validators. That 3.1 KB number isn't marketing โ it's the metafile output for this exact schema.
ArkType sits in between. It doesn't tree-shake as aggressively as Valibot because its type-level string syntax ("string", "number>0") needs a parser and a compiler bundled in, but it doesn't drag in Zod's full chained-API surface either.
The trade-off that doesn't show up in the table: Valibot's tree-shaking benefit evaporates once your schema actually uses most of the validator set (emails, URLs, regex, custom refinements, unions, records...). At that point it converges toward Zod's size. Measure your actual schema, not the marketing example.
โก Parse Speed: Nested Schemas Under Load
Bundle size is a one-time cost. Parse speed is what you pay on every request. For the 4-level nested User schema above, here's throughput (ops/sec, higher is better) on valid input:
text
Valid payload, 50,000 iterations:
ArkType 1,842,113 ops/sec ยฑ1.2%
Valibot 612,440 ops/sec ยฑ0.9%
Zod 398,207 ops/sec ยฑ1.4%
Invalid payload (error path), 50,000 iterations:
ArkType 1,210,884 ops/sec ยฑ1.8%
Valibot 301,552 ops/sec ยฑ2.1%
Zod 142,933 ops/sec ยฑ2.6%
ArkType's advantage here isn't incidental โ it compiles your schema definition down to an optimized JS function at schema-creation time, so validating becomes closer to hand-written if statements than to generic tree-walking. Here's the same schema in all three, for context:
typescript
// Zod โ chained builder API, tree-walked at parse time
import { z } from "zod";
const userSchema = z.object({
id: z.string(),
email: z.string().email(),
age: z.number(),
roles: z.array(z.enum(["admin", "user", "guest"])),
address: z.object({
street: z.string(),
city: z.string(),
zip: z.string(),
geo: z.object({ lat: z.number(), lng: z.number() }).optional(),
}).optional(),
orders: z.array(z.object({
id: z.string(),
total: z.number(),
items: z.array(z.object({
sku: z.string(), qty: z.number(), price: z.number(),
})),
})),
});
typescript
// ArkType โ type-syntax string, compiled to a validator function
import { type } from "arktype";
const userSchema = type({
id: "string",
email: "string.email",
age: "number",
roles: "(\"admin\"|\"user\"|\"guest\")[]",
"address?": {
street: "string",
city: "string",
zip: "string",
"geo?": { lat: "number", lng: "number" },
},
orders: [{
id: "string",
total: "number",
items: [{ sku: "string", qty: "number", price: "number" }],
}, "[]"],
});
Valibot lands in the middle because it's still function-composition-based (no compile step), but each function is smaller and more monomorphic than Zod's, which the V8 JIT rewards.
๐งต Where It Actually Bites
- High-throughput API boundaries (gateways, queue consumers, anything validating thousands of payloads/sec): ArkType's compiled validators are a real, measurable win โ 4-5x Zod on this schema. If you're CPU-bound on validation at that scale, this isn't a rounding error.
- Edge functions / serverless cold starts: ArkType's compile step happens once per cold start, which adds latency the first invocation pays. If your functions are short-lived and scale-to-zero aggressively, that compile tax can offset the parse-speed win. Valibot's lighter bundle wins the cold-start race here.
- Frontend bundles: Valibot's tree-shaking is the only one of the three that meaningfully moves your shipped JS. If you're validating a login form and nothing else, 1.3 KB gzip vs 15.9 KB is not subtle on a mobile connection.
- Error message ergonomics: not measured here, but worth saying โ Zod's error formatting and ecosystem (zod-to-json-schema, tRPC integration, react-hook-form resolvers) is still the deepest. Speed isn't the only cost.
-
Migration cost: Zod's chained API is the most "obvious" to read and write. ArkType's string-based type syntax is fast but has a real learning curve, and TypeScript inference errors inside those strings are less helpful than a type error on a bad
.optional()call.
๐ง So Which One
If you're validating API request bodies at meaningful scale and can tolerate a less mainstream API: ArkType, measure your own cold-start cost first. If you're shipping schemas to the browser and bundle size is the actual budget constraint: Valibot, but check your tree-shaken size with your real schema, not a toy one. If you want the deepest ecosystem and speed genuinely isn't your bottleneck (it usually isn't): Zod remains the safe, boring, well-integrated choice โ and "boring" is underrated when a library needs to work with six other tools in your stack.
What does your own schema's tree-shaken size look like once you throw in regex refinements and unions โ has anyone actually measured Valibot staying small past the toy-example stage? Drop your esbuild --metafile numbers in the comments, I'd like to see if the pattern holds outside this synthetic benchmark.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.