💎 雨云 RainYun - 企业级云计算服务提供商
Zod logo

AOT 编译

编辑此页面

Zod 可以提前将模式编译成扁平、无循环的验证器,其运行速度比标准解析器快数倍。结果和错误完全一致。

import * as z from "zod";
 
const Player = z.object({
  username: z.string(),
  bio: z.string(),
  xp: z.number(),
  // ...20 more properties...
});
 
const CompiledPlayer = z.compile(Player);

像使用 Player 一样使用它:

Player.parse({ ... });
CompiledPlayer.parse({ ... }); // ~9x faster

CompiledPlayer 这样的已编译模式和其他任何 Zod 模式一样。已编译模式没有特殊规则。

  • 相同的方法:.parse().safeParse().extend().optional()
  • 相同的推断输入和输出类型
  • 相同的问题和错误消息

Zod 的完整测试套件会运行两次——一次正常运行,另一次则全局启用自动编译——以确保完全一致。

对象和元组等容器受益最大,因为编译会将运行时逐键遍历展开为扁平、无循环的验证逻辑,从而可以由 JS 引擎进行优化。

在共享的纳秒坐标轴上显示每次解析所需时间,标准解析器为灰色条,编译后的时间为其中的蓝色条:包含 10 个对象的数组从 377 ns 降至 68 ns(5.5 倍),20 键对象从 301 ns 降至 38 ns(7.8 倍),包含 10 个字符串的数组从 241 ns 降至 33 ns(7.3 倍),包含 3 个对象的联合从 190 ns 降至 36 ns(5.3 倍),3 元素元组从 119 ns 降至 33 ns(3.6 倍),5 键严格对象从 117 ns 降至 32 ns(3.7 倍),可辨识联合从 92 ns 降至 27 ns(3.4 倍),5 键对象从 76 ns 降至 28 ns(2.8 倍);编译后最高快 7.8 倍
每次解析所需时间,标准解析器与编译后对比——越低越好(基准测试

有两种启用方式。

z.compile()

编译单个模式并返回一个编译后的副本。原模式不会改变。

派生出模式的方法(.refine().extend().optional().meta()……)会返回未编译的模式。请编译最终模式,而不是中间模式:

// ❌ the .refine() result is not compiled
const schema = z.compile(z.string()).refine((val) => val.length > 1);
 
// ✅ compile last
const schema2 = z.compile(z.string().refine((val) => val.length > 1));

import "zod/compile"

全局启用编译。在此导入之后构造的每个模式,都会在首次解析时自动编译。

import "zod/compile"; // must come before modules that define schemas
import * as z from "zod";
 
const schema = z.object({ name: z.string() });
schema.parse({ name: "ok" }); // compiled on first parse

编译是惰性的,因此只有实际进行解析的模式才会被编译。

它也可以作为 Node.js CLI 标志使用,这样可以确保它在任何模块定义模式之前运行:

node --import zod/compile app.js   # ESM
node --require zod/compile app.cjs # CommonJS

或者在 bunfig.tomlnub.jsonc 中设置 preload

nub.jsonc
{
  "preload": ["zod/compile"]
}

此导入方式用于应用程序,而不是库。

工作原理

在底层,z.compile() 会遍历整个模式一次,并生成一段高度优化的扁平、无循环 JavaScript 代码,其验证输入的速度远快于标准运行时验证器。这段代码可以通过 new Function() 执行(实际上是功能更强大的 eval),作为快速路径验证器使用。模式会利用它来“快速检查”有效性,在验证失败时回退到常规运行时逻辑,以提供详细的错误信息。

请看这个简单的 Point schema:

const Point = z.object({
  x: z.number(),
  y: z.number()
});

这是为它生成的代码片段:

const isPoint = new Function("input", `
  if (typeof input !== "object" || input === null) return false;
  if (typeof input.x !== "number") return false;
  if (typeof input.y !== "number") return false;
  return true;
`);
 
isPoint({ x: 1, y: 2 }); // true
isPoint({ x: "1" });     // false

对于绝大多数输入,生成的函数会使用 JavaScript 能够表达的最快逻辑来验证数据:直接执行 typeof 检查和属性读取,中间没有解释器介入。当它无法处理某个输入时,Zod 会回退到标准解析器。

这是 Zod 为上面的 Player schema 生成的函数:

if (typeof input !== "object" || input === null || Array.isArray(input)) return INVALID;
const v0 = input["username"];
if (typeof v0 !== "string") return INVALID;
const v1 = input["bio"];
if (typeof v1 !== "string") return INVALID;
const v2 = input["xp"];
if (typeof v2 !== "number" || !Number.isFinite(v2)) return INVALID;
const v3 = { "username": v0, "bio": v1, "xp": v2 };
return v3;

借助 new Function() 的能力,这一切会在运行时于进程内完成。无需集成到你的构建系统中。

编译后的 schema 只是对现有 schema 的纯增量增强。它附加了用于检查有效输入的预编译快速路径。检测到无效数据时,它会返回 INVALID 符号,以表示解析应回退到未编译的解析器。这在结构上避免了编译版本与未编译版本之间错误报告的细微偏差。

对于无效输入,回退逻辑会运行未编译的模式,因此错误来自未编译模式。由此带来两个结果:

  • 无效输入需要同时经过快速路径和回退逻辑,因此编译不会加快失败情况。
  • 对于有效输入,细化和转换只会运行一次;对于无效输入,最多运行两次。

不支持的模式

有些功能无法编译,或者从编译中无法受益。在这些情况下,z.compile() 会退出编译并原样返回原模式:

const Schema = z.string().refine(async (val) => isAvailable(val));
 
z.compile(Schema); // returns Schema itself, uncompiled
  • async 细化、转换和检查
  • z.xor()
  • 递归模式
  • z.coerce.*
  • 带有自定义 when 的检查
  • 使用回调作为参数的 .catch()(带常量的 .catch(value) 会正常编译)

在对象、数组、元组、记录或交集中,不支持的子模式会使用标准解析器运行,而外围结构仍会保持编译状态。包含不支持成员的联合、.catch() 回调,或子树中任意位置存在异步逻辑,都会使整个模式回退。

编码(z.encode()、“backward”方向的 codec)和异步解析始终使用标准解析器。

传入 strict 可以让其抛出错误而不是回退——例如,用于确认热路径上的模式确实完成了编译:

z.compile(Schema, { strict: true }); // throws ZodCompileAsyncError

对于异步模式会抛出 ZodCompileAsyncError,其他所有情况会抛出 ZodCompileUnsupportedError。这两种错误都只会在 strict 下抛出。

内容安全策略

编译使用 new Function,而它在 CSP/不允许使用 eval 的环境中不可用。设置 jitless 后,全局模式会停用:

z.config({ jitless: true });

直接调用 z.compile() 是显式选择启用,因此无论 jitless 的设置如何,它都会尝试生成代码。如果环境拒绝 new Function,该模式会像其他拒绝编译的情况一样,以未编译状态返回。

包体积

编译器包含大量代码,通过 z.compile()"zod/compile" 调用它意味着编译器会被包含在你的包中。它会增加约 7 KB 的 gzip 压缩体积(压缩后 28 KB)。从未调用 z.compile() 或导入 zod/compile 的包无需承担任何开销;打包时它会被完全 tree-shaking。

包(四键对象模式)不含编译器含编译器
Zod24.1 KB31.1 KB
Zod Mini4.6 KB13.2 KB

基准测试

收益会随着模式复杂度增加而提升。这里的每个模式都是在紧密循环中单独测量的——这是标准解析器的最佳情况——因此其倍数低于页面顶部图表中的结果(基准测试)。

模式加速比
对象,5 个键1.8 倍
对象,10 个键2.2 倍
对象,20 个键5.0 倍
对象,50 个键10.2 倍
元组,1 个项2.2 倍
元组,3 个项2.5 倍
元组,5 个项3.0 倍
元组,10 个项3.7 倍

基于 Moltar 基准测试 fixture 的结果,将编译和未编译的 Zod 与其他库进行比较。parseSafe 类别会返回一个剥离未知键的新对象。

Moltar 基准测试 fixture 上每秒操作次数的柱状图,parseSafe 类别:Zod 4 编译后 47.5M,typia 45.3M,Zod 4 11.6M,valibot 1.8M,effect 1.7M,Zod 3 1.2M,arktype 152k,yup 121k
Moltar 基准测试 fixture 上的吞吐量(parseSafe:返回一个剥离未知键的新对象)——越高越好(基准测试

assertLoose 类别返回布尔值并允许未知键。Zod 会通过 z.validate() 运行它。

Moltar 基准测试 fixture 上每秒操作次数的柱状图,assertLoose 类别:typia 74.9M,arktype 66.2M,Zod 4 编译后 60.6M,Zod 4 6.5M,valibot 1.9M,effect 1.7M,Zod 3 1.2M,yup 124k
Moltar 基准测试 fixture 上的吞吐量(assertLoose:返回布尔值,允许未知键)——越高越好(基准测试

On this page