Cómo acelerar la validación con Zod mediante esquemas compilados
Vea cómo los esquemas compilados de Zod aceleran la validación, cuándo importa la mejora y cómo gestionar costes, esquemas no compatibles y restricciones CSP.
A partir de Zod 4.5.0, z.compile() convierte un esquema en un validador de JavaScript especializado. Verifica las entradas válidas más rápido y devuelve exactamente los mismos resultados y errores que el esquema sin compilar.
Si tu handler analiza el mismo cuerpo de petición de gran tamaño miles de veces por minuto, una capa de validación que recorre el árbol del esquema en cada llamada acaba apareciendo en tus perfiles de CPU. La versión 4.5 de Zod incorporó una solución para ello. Este artículo da continuidad a la guía de OpenReplay sobre validación de datos en TypeScript con Zod. Explica cómo activar la compilación, cuánto cuesta, en qué casos la mejora es lo bastante grande como para importar y qué ocurre bajo una Content Security Policy estricta o en entornos de ejecución como Cloudflare Workers.
Puntos clave
z.compile(schema)genera un validador sin bucles ni recorridos con ramificaciones y lo ejecuta mediantenew Function(). Las entradas válidas se verifican con una simple secuencia de comprobacionestypeofen lugar de recorrer el árbol del esquema paso a paso.- Las entradas no válidas apenas ganan nada, porque un análisis fallido ejecuta primero la ruta rápida y después el parser estándar completo para construir los errores.
- En el benchmark de bucle cerrado del propio Zod, los objetos compilados con 5, 10, 20 y 50 claves son 1,8x, 2,2x, 5,0x y 10,2x más rápidos, por lo que los esquemas pequeños apenas se benefician.
- El compilador añade unos 7 KB comprimidos con gzip a cualquier bundle que llame a
z.compile()o importezod/compile. { strict: true }hace que los esquemas no compilables lancenZodCompileAsyncErroroZodCompileUnsupportedErroren lugar de recurrir silenciosamente al parser estándar.
¿Cómo funcionan los esquemas compilados de Zod?
Un esquema compilado de Zod valida la entrada mediante código generado en lugar de recorrer el árbol del esquema. Zod lee el esquema completo una sola vez, escribe un fragmento breve de JavaScript sin bucles y lo convierte en una función con new Function(). A partir de ese momento, las entradas válidas se verifican leyendo cada propiedad y comprobando su typeof, una línea tras otra. Si la ruta rápida rechaza una entrada, Zod ejecuta sobre ella el parser estándar. Por eso un esquema compilado informa de los mismos problemas y mensajes de error que el original. Sigues usando .parse(), .safeParse() y los mismos tipos inferidos.
Cómo activar la compilación
Hay dos formas de activarla: compilar esquemas concretos con z.compile() o compilar todos los esquemas de la aplicación importando zod/compile de forma global. La compilación por esquema te da un control preciso sobre las rutas críticas (hot paths). El modo global solo requiere una línea.
Por esquema con z.compile()
z.compile() devuelve un nuevo esquema compilado. El esquema que le pasas no se modifica. Cualquier método que construya un nuevo esquema a partir de uno compilado, como .refine(), .extend() u .optional(), devuelve un esquema sin compilar. Construye primero el esquema definitivo y compílalo al final:
import * as z from "zod";
const Base = z.object({
id: z.string(),
type: z.string(),
createdAt: z.number(),
});
const notInFuture = (e: { createdAt: number }) => e.createdAt <= Date.now();
// ❌ .refine() returns a new schema, and it is not compiled
const Wrong = z.compile(Base).refine(notInFuture);
// ✅ finish the schema, then compile it
const WebhookEvent = z.compile(Base.refine(notInFuture));
const result = WebhookEvent.safeParse(payload);
Nada te avisa en tiempo de ejecución de que Wrong no está compilado. Valida correctamente, solo que más despacio. La opción strict (que se explica más adelante) tampoco lo detecta, porque solo lanza un error cuando un esquema no se puede compilar en absoluto. La costumbre segura es que z.compile() sea la última llamada de la cadena.
De forma global con zod/compile
Con la compilación por defecto, Zod compila cada esquema que crees después de la importación. Lo hace de forma diferida (lazy), en el primer análisis de cada esquema:
import "zod/compile"; // must run before any module that defines schemas
import * as z from "zod";
const User = z.object({ name: z.string() });
User.parse({ name: "ok" }); // compiled on first parse
En ESM es fácil equivocarse con el orden de carga, así que deja que sea el entorno de ejecución quien cargue el módulo primero:
node --import zod/compile app.js # ESM
node --require zod/compile app.cjs # CommonJS
Quienes usen Bun pueden añadir zod/compile en preload dentro de bunfig.toml. El modo global está pensado para aplicaciones. Las librerías no deberían activarlo en nombre de sus consumidores.
¿Cuánto cuesta la compilación en Zod?
La compilación de esquemas en Zod tiene tres costes y aporta poco con entradas no válidas. Un análisis rechazado ejecuta la ruta rápida, falla y luego ejecuta el parser estándar completo, que consume casi todo el tiempo. Los tres costes son:
- Trabajo de compilación único por cada esquema, que se realiza al compilarlo o, en modo global, en su primer análisis.
- Tamaño del bundle. El compilador añade unos 7 KB comprimidos con gzip (28 KB minificados). Según la tabla del propio Zod, un esquema de objeto con cuatro claves ocupa 31,1 KB comprimidos con el compilador, frente a 24,1 KB sin él. En Zod Mini el salto es mayor: de 4,6 KB a 13,2 KB. Los bundles que nunca llaman a
z.compile()ni importanzod/compileeliminan por completo el compilador durante el tree-shaking. - Doble ejecución en caso de fallo. Un refinamiento o una transformación se ejecuta una sola vez cuando la entrada es válida, pero puede ejecutarse dos veces cuando no lo es. Un refinamiento que registre logs, lleve un recuento o escriba datos lo hará dos veces ante un webhook rechazado.
En el punto de entrada de las peticiones, con tráfico sostenido, el trabajo inicial se amortiza rápidamente. En un script puntual o en una CLI que analiza un único archivo de configuración, puede que el trabajo de compilación y los bytes adicionales nunca se recuperen. El tráfico dominado por entradas incorrectas, como sondeos de bots o firmas de webhook falsificadas, también compensa poco.
¿Dónde se notan las mejoras de rendimiento de Zod?
Las mejoras de rendimiento que aporta la compilación crecen con el tamaño del esquema. Los objetos y las tuplas con muchos elementos son los que más se benefician, porque el código generado comprueba cada clave una tras otra sin el bucle por clave del parser estándar. El benchmark del propio Zod mide cada esquema por separado, de forma repetida en un bucle cerrado. En ese escenario el parser estándar ofrece su mejor rendimiento, por lo que las mejoras son menores que las del gráfico principal que aparece al principio de esa misma página.
| Esquema | Aceleración |
|---|---|
| Objeto, 5 claves | 1,8x |
| Objeto, 10 claves | 2,2x |
| Objeto, 20 claves | 5,0x |
| Objeto, 50 claves | 10,2x |
| Tupla, 1 elemento | 2,2x |
| Tupla, 3 elementos | 2,5x |
| Tupla, 5 elementos | 3,0x |
| Tupla, 10 elementos | 3,7x |
Un formulario de inicio de sesión con tres campos no notará la diferencia. Un payload de eventos con 50 claves que se analiza en cada petición, sí. Las cifras más elevadas, como el “hasta 44x” (y hasta 46x con entradas rechazadas) que se atribuye a zod-compiler, proceden de herramientas de terceros independientes que generan validadores en tiempo de compilación. No describen el compilador en tiempo de ejecución integrado en Zod.
¿Qué esquemas de Zod no se pueden compilar?
Algunas funcionalidades de los esquemas de Zod no se pueden compilar. Cuando z.compile() encuentra una, no lanza ningún error: simplemente te devuelve, sin avisar, el esquema que le pasaste sin compilar. La lista de elementos no compatibles determina si pierdes la compilación del esquema completo o solo la de un hijo:
| Funcionalidad | Efecto |
|---|---|
| Refinamientos, transformaciones o comprobaciones asíncronas en cualquier punto del árbol | Todo el esquema recurre al parser estándar |
.catch() con un callback (.catch(value) sí se compila) | Todo el esquema recurre al parser estándar |
| Unión con un miembro no compatible | Todo el esquema recurre al parser estándar |
z.xor(), esquemas recursivos, z.coerce.*, comprobaciones con un when personalizado | No se compilan |
| Hijo no compatible dentro de un objeto, array, tupla, record o intersección | Solo ese hijo usa el parser estándar |
La compilación nunca se aplica a z.encode() ni al análisis asíncrono. Ambos pasan siempre por el parser estándar. Si tus handlers llaman a safeParseAsync, compilar el esquema no les aporta nada.
Para evitar que una modificación posterior desactive silenciosamente la compilación en una ruta crítica, compila en CI con { strict: true }:
import { test } from "node:test";
import assert from "node:assert/strict";
import * as z from "zod";
import { WebhookEvent, OrderBody } from "../src/schemas.js";
test("hot-path schemas compile", () => {
for (const schema of [WebhookEvent, OrderBody]) {
assert.doesNotThrow(() => z.compile(schema, { strict: true }));
}
});
test("async refinements are rejected under strict", () => {
const Handle = z.string().refine(async (v) => v.length > 2);
assert.throws(() => z.compile(Handle, { strict: true })); // ZodCompileAsyncError
});
Con strict activado, un esquema asíncrono lanza ZodCompileAsyncError y cualquier otro esquema que Zod no pueda compilar lanza ZodCompileUnsupportedError. Sin strict, no se lanza ninguno de los dos errores.
¿Funciona z.compile() con CSP y en Cloudflare Workers?
z.compile() falla de forma segura, pero no aporta nada allí donde está prohibida la generación dinámica de código. new Function() queda bloqueado en cualquier página cuya Content Security Policy no incluya 'unsafe-eval' en script-src (o en default-src cuando no hay script-src). Zod también menciona Cloudflare Workers como un entorno en el que new Function() está bloqueado. Si configuras jitless, el modo global se desactiva automáticamente:
// config.ts: import this module before any module that defines schemas
import * as z from "zod";
z.config({ jitless: true });
Llamar tú mismo a z.compile() es distinto. Zod lo interpreta como una petición explícita, así que intenta generar código incluso con jitless activado. Si el entorno bloquea new Function, simplemente recibes el esquema sin compilar. La validación sigue funcionando. Sin embargo, habrás incluido unos 7 KB comprimidos de un compilador que nunca podrá ejecutarse, así que deja zod/compile y z.compile() fuera de los builds destinados a esos entornos.
Conclusión
La compilación acelera la verificación de entradas válidas sin alterar los resultados ni los errores, y la mejora crece con el ancho del esquema. Empieza por perfilar el punto de entrada de tus peticiones. Compila tus esquemas más amplios y con más tráfico al final de su cadena de construcción, y añade un test con strict para garantizar que sigan siendo compilables. Mantén el compilador fuera de los builds destinados a entornos de ejecución y políticas CSP que bloqueen new Function. Instala la versión actual de Zod 4 en lugar de fijar la 4.5.0, para beneficiarte de las correcciones del compilador.
Preguntas frecuentes
¿Existe en Zod una forma más rápida que safeParse de rechazar entradas no válidas?
Sí. Zod 4.6 añadió .validate(), que solo indica si la entrada es válida. Omite la construcción de un ZodError, por lo que rechazar entradas incorrectas cuesta muy poco. En un esquema compilado con una entrada no válida, puede ser hasta 35 veces más rápido que .safeParse().success. Además, estrecha el tipo: cuando devuelve true, TypeScript trata el valor como el tipo de entrada del esquema. Úsalo cuando solo necesites una respuesta de sí o no, y mantén .safeParse() cuando necesites los detalles del error.
¿Puedo usar un parser de Zod precompilado en entornos que bloquean new Function?
Sí, a partir de Zod 4.6. z.compile() realiza dos tareas: genera un parser y lo asocia al esquema. z.withParser() solo realiza la segunda. Le proporcionas un parser generado en otro lugar, por ejemplo en un paso de build o con un compilador nativo, y lo asocia siguiendo las mismas reglas que z.compile(). Como el código generado se distribuye como JavaScript convencional, el entorno de ejecución nunca necesita new Function.
¿Cuál es la diferencia entre z.compile() y zod-compiler?
z.compile() está integrado en Zod y genera validadores en tiempo de ejecución con new Function(), dentro de tu proceso. zod-compiler (gajus/zod-compiler) es una herramienta de terceros independiente que genera validadores en tiempo de compilación, mediante plugins de bundler para Vite, webpack, esbuild, Rollup y otros, o a través de una CLI. Su salida en tiempo de compilación es código convencional sin eval, por lo que las reglas CSP no pueden desactivarla. No obstante, su modo opcional en tiempo de ejecución sí utiliza new Function. La versión actual de zod-compiler requiere Zod 4.5 o posterior, y su rama 1.x es compatible con Zod 4.0 a 4.4.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k