Key Takeaway
For bundle-sensitive frontends and edge functions, Valibot beats Zod on size: Valibot v1 ships around 1.3 kB gzipped because its schema and validation functions are separate, tree-shakeable modules. Zod v4 standard bundles everything at roughly 13 kB; the new zod/v4-mini export trims that to about 3.9 kB, but Valibot's modular pipe design still wins.
I reach for Zod on almost every TypeScript project I run. It is the default for a reason: the API is comfortable, the type inference is excellent, and the ecosystem around it is enormous. But on a recent project I was shipping validation into a Cloudflare Worker and watching the bundle budget like a hawk, and Zod's weight became a real problem. That is when I started taking Valibot seriously, and the difference is not marginal.
The core question is not which library validates better. Both are correct, both infer types cleanly, both cover the same shapes. The question is what ends up in your production bundle, and that comes down to a single architectural decision each library made about how you compose a schema.
Why Zod's method chaining resists tree-shaking
Zod's classic API is method chaining on a single object. You import one thing and call methods off it. That ergonomic API is exactly what makes it hard to shrink. When you write the import below, the bundler pulls in the whole class graph because every method lives on the same object; a bundler cannot prove that you never call the string date parser or the discriminated union just because you did not type it here.
// Zod classic — one import, everything comes with it
import { z } from "zod";
const LoginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
type LoginData = z.infer<typeof LoginSchema>;
// The whole Zod runtime (~13 kB gzipped) is now in your bundle,
// even though this schema uses maybe five features.This is not a flaw so much as a trade-off. Method chaining reads beautifully and it is the reason so many of us fell in love with Zod. But the object that carries all those methods is one indivisible unit as far as the bundler is concerned.
How Valibot's modular pipe design changes the math
Valibot inverts the design. Instead of methods on a class, every schema and every validation is a standalone function you import by name. Constraints are composed with a pipe function that threads a value through a sequence of actions. Because each action is its own exported function, the bundler keeps only the functions you actually reference and drops the rest. This is textbook tree-shaking, and it is why Valibot v1 lands around 1.3 kB gzipped for a typical schema.
// Valibot — import only the functions you name
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
type LoginData = v.InferOutput<typeof LoginSchema>;
// Parse the same way you would with Zod
const result = v.parse(LoginSchema, { email: "a@b.com", password: "secret12" });The wildcard import (import star as v) looks like it pulls in everything, but it does not. Modern bundlers like esbuild, Rollup, and Rolldown resolve which named exports you actually use and shake out the rest. The namespace import is just an ergonomic convenience, not a bundle-size cost.
Zod v4 fought back with zod/v4-mini
Zod's maintainer clearly heard the bundle-size complaints. Zod v4, which landed in early 2025, introduced a separate zod/v4-mini export that abandons method chaining in favour of standalone check functions, much closer to Valibot's style. It brings the gzipped cost down to roughly 3.9 kB, a big improvement over the ~13 kB standard build. The catch is that the API is noticeably more verbose than classic Zod, and you give up some of the chaining ergonomics that made Zod pleasant in the first place.
// zod/v4-mini — standalone checks, no method chaining
import * as z from "zod/mini";
const LoginSchema = z.object({
email: z.string().check(z.email()),
password: z.string().check(z.minLength(8)),
});
type LoginData = z.infer<typeof LoginSchema>;
// ~3.9 kB gzipped — smaller than classic Zod,
// still roughly 3x heavier than Valibot for the same schema.The comparison at a glance
| Dimension | Valibot v1 | Zod v4 (classic / mini) |
|---|---|---|
| Gzipped size (typical schema) | ~1.3 kB | ~13 kB classic / ~3.9 kB mini |
| Composition model | Standalone functions + pipe() | Method chaining / .check() in mini |
| Tree-shakeable | Yes, by design | No (classic) / Yes (mini) |
| API ergonomics | Explicit, more imports | Very fluent (classic) / verbose (mini) |
| Ecosystem & adapters | Growing, good but smaller | Huge, de facto standard |
| Best fit | Edge, browser, size-critical | Node servers, existing Zod code |
Which one I actually pick
- Server-side Node or NestJS APIs: I stay on Zod. Bundle size is irrelevant when the code runs on a long-lived server, and Zod's ecosystem, adapters, and familiarity win easily.
- Cloudflare Workers, Vercel Edge, or anything with a cold-start and a size limit: Valibot, without hesitation. Shaving ten-plus kilobytes off every deploy matters when the platform caps you.
- Client-side form validation in a browser bundle: Valibot if the page is performance-critical, otherwise either is fine.
- A shared schema used on both frontend and backend: pick the one whose weight the frontend can tolerate, since the backend does not care either way.
Do not migrate a large existing Zod codebase to Valibot just to save kilobytes on the server. The refactor cost, the loss of Zod-specific adapters, and the risk of subtle validation differences almost never pay off unless bundle size is a hard, measured constraint. Measure first, then decide.
The honest summary: Valibot won the bundle-size argument so convincingly that Zod restructured its own API to answer it. That is the strongest endorsement Valibot could get. But Zod is not standing still, and for most server work its ergonomics and ecosystem still make it my default. Choose based on where the code runs, not on which library is trendier this quarter.

