【免费下载链接】zod-compiler
Compile Zod schemas into zero-overhead validation functions at build time. Works with Vite, webpack, esbuild, Rollup, etc
zod-compiler 是一个在构建期把 Zod schema 编译成零开销校验函数的开源编译器(支持 Vite、webpack、esbuild、Rollup 等)。它的核心性能来自双路径设计:Fast Path生成一条零分配的&&布尔链来秒判合法输入,Slow Path生成一条只在失败时才真正执行、负责收集 Zod 错误列表的"冷路径"。本文带你读懂这套&&链的生成原理,以及两条路径在源码中如何分工协作。
一、为什么一个 schema 要编译出"两条路"?
常规运行时校验(包括原生 Zod)每次safeParse都要走完整的检查流程,合法输入也要付出分配错误数组、构造结果对象的开销。zod-compiler 的思路是按输入结果的冷热拆分:
| Fast Path(热路径) | Slow Path(冷路径) | |
|---|---|---|
| 面向输入 | 合法输入 | 非法输入 |
| 产物形态 | 一条布尔表达式(&&链) | 一段收集issues的遍历代码 |
| 内存分配 | 零分配 | 只在读.error时执行 |
| 执行时机 | 每次校验 | 延迟到需要错误详情时 |
| 源码位置 | fast-path.ts | slow-path.ts |
编译产物长这样(示意,省略 preamble):
function safeParse_user(input){ if(__fc_0(input)){return{success:true,data:input};} return __zcFinD(__sw_1,input); // 延迟执行慢路径 }合法输入在第一个if就直接返回,不分配任何错误对象;非法输入才进入__zcFinD包装的慢路径——而如果你从不读取.error,这段代码连执行都不会执行。这正是"拒绝输入最高提速 140x"的来源(见 README.md 中的 Benchmark)。
二、Fast Path:零分配 && 链是怎么拼出来的
2.1 类型注册表:一张表决定"能不能走快路"
fast-path.ts 的核心是一张fastRegistry,把每种 SchemaIR 节点映射到一个生成函数:
- 返回值是布尔表达式字符串的,就能参与
&&链; - 注册为
null的类型(effect、fallback、stringBool)静态不合格,一出现整条快路作废; - 动态不合格的(如
coerce、.trim()等改写值的效果)会在生成时返回null,触发整树回退。
这种"全有或全无"(all-or-nothing)策略保证了快路语义:fc(input) === true当且仅当 schema 接受 input,因此它可以被安全地直接装进.is()类型守卫。
2.2 一个 object 的 && 链长什么样
以 schemas/object.ts 的fastObject为例,生成的表达式结构是:
typeof input==="object" && input!==null && !Array.isArray(input) && input["name"] in input && typeof input["name"]==="string" && input["name"].length>=1 && __zcEmail(input["email"])注意两个关键细节:
- 检查按运行成本升序排列。fast-size.ts 中的
orderByRuntimeCost给每个子节点估价(正则string_format权重 100,长度比较权重 1)。合法输入反正全都要跑,排序不花钱;但非法输入在第一个 false 处短路——把z.email()排到kind字面量比较之后,实测 3 分支 union 的拒绝校验从 102 ns 降到 6 ns。字符串内部的检查排序同理,见 schemas/string.ts 里的checkPriority(长度 O(1) 最便宜,正则最贵)。 - 属性只读取一次。
optional/nullable/default这类包装器如果朴素生成,同一个input["a"]会被读两遍;context.ts 中的fastSentinelWrapper会把它绑定进局部变量:((__w_1=input["a"])===undefined||(...)),避免 V8 在巨对象(megamorphic)场景下重复做属性查找,实测整体快 1.1–1.7x。
2.3 超大 schema 自动拆分成 __fo_N 辅助函数
深层嵌套的 schema 如果全部内联,单条快路函数会超过 V8 TurboFan 的字节码优化预算(约 60KB),退化到较慢的 Maglev 层,实测慢 3–4.5 倍。于是generateFast内置了尺寸门控提取:
- 正在组装的函数逼近
EXTRACT_CAP(38,000 字符)时,把下一个够大(MIN_EXTRACT1,200 字符)的可提升子树(object/union/tuple 等,见HOISTABLE)抽成独立的布尔辅助函数function __fo_N(p){return <expr>;},原位置替换为一次调用; - 小 schema 永远碰不到上限,输出与完全内联形式逐字节一致。
对应测试见 tests/core/codegen/fast-extract.test.ts。
2.4 连正则都做了"免构建"优化
快路里的正则不是每次解析都new RegExp:context.ts 的emitRegex会去重、池化,甚至跨校验器共享;而 Zod 默认的 email 模式根本不走正则——由专用的__zcEmail线性扫描器替代(见 well-known-regex.ts),比原模式快约 1.45 倍。
三、Slow Path:只写一份、按需执行的冷路径
3.1 结构与快路"镜像对称"
slow-path.ts 的slowRegistry覆盖全部节点类型(包括快路不合格的effect)。每个生成函数拿到的上下文(SlowGen)包含input / output / path / issues四个槽位,递归时通过visit传递。与快路镜像的地方在于:
- 快路是
&&/||的表达式,失败即短路; - 慢路是
if(...){issues.push(...);}的语句序列,不短路、收集所有错误,以复现 Zod 的完整 issue 列表(顺序也是语义的一部分)。
错误对象发射统一走 emit-issue.ts,保证 issue 结构与 Zod 逐字节一致。
3.2 重复子树只生成一次
同一文件里多个 schema 复用相同子结构(比如一个Address对象被三处引用)时,dedupe.ts 会把它生成一次为模块级函数__zcSw_N(input, path, issues),其余位置只发调用。慢路占产物体积的 64–77%,这一步让重复模块的产物缩了 19–28%(gzip 后 10–18%)。而快路从不共享——它要保持完全内联,否则函数多态会毁掉内联优化。
3.3 延迟执行:__zcFinD 是零成本的关键
回看第一节的产物:失败分支不是"立即遍历一遍再构造 ZodError",而是return __zcFinD(__sw_1, input)——把整个慢路打包进一个惰性错误访问器。没人读.error,慢路就永远不跑。这是 zod-compiler 与"先跑两遍校验"的朴素方案最本质的区别。
四、编排全景:generateValidator 如何调度两条路径
两条路径的总入口是 codegen/index.ts 的generateValidator,流程为:
- 快照状态→ 2.试生成快路(
createFastGen+generateFast)→ 3.生成慢路(createSlowGen+generateSlow)→ 4.拼装safeParse。
几个值得注意的工程细节:
- 回滚机制:快路生成是全程"可回滚"的——它产生的 preamble 声明、正则缓存先记账,一旦走到不合格的节点,整个快路被丢弃并恢复快照,让慢路从干净状态重新声明,避免输出里留下悬空的
__fo_引用; fastTotal标记:无突变、快路完全等价于 Zod 判定的 schema 会把快路函数直接装为.is()(零分配的类型守卫);带default/catch的"部分快路"则不发布,因为快路 false 不等于拒绝;- compact 模式:对无突变的 schema,干脆不发射慢路,失败时委托给保留的原始 Zod schema 产错,产物再小 64–77%。
五、哪些 schema 会失去 Fast Path?
按 fast-path.ts 的注册表,规则很清晰:
| 情况 | 结果 |
|---|---|
transform、superRefine(effect)、fallback、stringBool | 静态不合格,整树走慢路 |
coerce、.trim()/.toLowerCase()等改写值 | 动态不合格,快路被回滚 |
default/catch | "部分快路":快路 true 可短路,false 仍需慢路兜底 |
refine且谓词零捕获 | 合格:谓词按源码托管进 preamble,参与&&链 |
六、读源码的推荐路线 🧭
| 想理解 | 从这里读起 |
|---|---|
| 快路总控与尺寸提取 | fast-path.ts |
| 慢路总控与 visit 语义 | slow-path.ts |
| 快/慢上下文、临时变量与常量池 | context.ts |
| 成本估价与检查排序 | fast-size.ts |
| 两路如何被编排进 safeParse | index.ts |
| object/union 的链式生成 | schemas/object.ts、schemas/union.ts |
| 字符串检查与 email 扫描器 | schemas/string.ts、well-known-regex.ts |
| 慢路去重(__zcSw_N) | dedupe.ts |
配合 tests/core/codegen/ 下的测试(如fast-locals.test.ts、dedupe.test.ts),可以快速验证每个优化点的行为边界。
总结
zod-compiler 的 Fast/Slow Path 设计可以浓缩成三句话:热路用一条按成本排序、零分配的&&布尔链秒判合法性;冷路把错误收集整段延迟,没人读就不执行;两条路都从同一份 SchemaIR 生成,保证判定与 Zod 逐字节一致。理解这套机制后,你既能解释 43x/140x 的性能数字从何而来,也能预判哪些 schema 写法会让自己的校验落回慢路。
【免费下载链接】zod-compiler
Compile Zod schemas into zero-overhead validation functions at build time. Works with Vite, webpack, esbuild, Rollup, etc
相关推荐
终极ACG技术指南:探索awesome-acg项目中的200+精选工具与资源
终极ACG技术指南:探索awesome acg项目中的200+精选工具与资源 awesome acg是一个精心策划的开源项目,汇集了超过200个与动画、漫画和游
终极Zod核心原理大揭秘:从源码看TypeScript类型推断实现
终极Zod核心原理大揭秘:从源码看TypeScript类型推断实现 Zod是一个功能强大的TypeScript first数据验证库,它允许开发者通过简洁的AP
后端API设计Windows系统优化终极指南:三分钟让你的电脑焕然一新
Windows系统优化终极指南:三分钟让你的电脑焕然一新 你是否曾为新电脑的繁琐设置而头痛?是否厌倦了Windows系统臃肿、启动缓慢的问题?今天我要为你介绍一
运维桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考