Zod 4.5: compilar, validar y safeParse: elegir un analizador por límite
Cómo hacer pruebas de rendimiento de los analizadores compilados de Zod 4.5 y del camino rápido de validación booleana con cargas útiles válidas e inválidas, y dónde se encuentra cada API sin perder las rutas de error.
Fuentes: Zod 4.5 z.compile() Cut My Parse Time. Invalid Payloads Got Faster Than Valid Ones.
Si un manejador de rutas comienza con schema.safeParse(body), se paga el costo de interpretación de Zod en cada solicitud. Zod 4.5, anunciado el 28 de agosto de 2026, añade z.compile() para que parte de ese trabajo se realice una sola vez, además de un camino rápido de validación booleana que omite la generación de detalles de errores. Un análisis más rápido solo es útil si la información de error de la que depende la interfaz de usuario se mantiene, por lo que esta guía muestra cómo medir el tiempo de ejecución de las tres opciones con un esquema realista, tanto con entrada válida como inválida, y cómo decidir qué API corresponde en cada límite de una aplicación.
Qué afirma realmente la versión 4.5
Es útil separar las declaraciones oficiales del lanzamiento de los titulares que se difunden posteriormente. Según el anuncio de 4.5:
- cualquier esquema puede compilarse previamente con
z.compile(schema), lo que acelera el análisis;
z.validate() es una vía rápida de tipo booleano, que según se describe es hasta 16 veces más rápida con datos inválidos porque no construye el árbol completo de problemas;z.string() pasó de aproximadamente 7.5 kB retenidos en la versión 4.4 a 784 bytes en la versión 4.5.Considere estas cifras como los valores de referencia de los mantenedores. “Hasta 16 veces” describe el mejor caso, así que entiéndalo como la dirección de la mejora y no como el resultado que debe esperar. Esta interfaz de API es nueva en el momento de escribir esto, por lo que confirme los nombres y las firmas según la documentación actual de Zod y su versión instalada antes de confiar en cualquier información presentada.
Un esquema realista para probar
Los micropruebas en una sola llamada a z.string() no proporcionan mucha información sobre una aplicación. Una prueba más fiable utiliza un esquema similar al tráfico real: una factura con un ID, un correo electrónico, un total, una moneda, una lista de ítems y un registro de metadatos opcional. Nada se simplifica para que el compilador muestre un mejor rendimiento.
import * as z from "zod";
export const Invoice = z.object({
id: z.string().uuid(),
email: z.string().email(),
total: z.number().nonnegative(),
currency: z.enum(["USD", "EUR", "BDT"]),
lines: z
.array(
z.object({
desc: z.string().min(1),
qty: z.number().int().positive(),
unit: z.number().nonnegative(),
}),
)
.min(1),
meta: z.record(z.string(), z.string()).optional(),
});
export const CompiledInvoice = z.compile(Invoice);
export type Invoice = z.infer<typeof Invoice>;
Observe tres cosas. CompiledInvoice se crea una sola vez, en el ámbito del módulo. El tipo deducido proviene aún del esquema original Invoice, ya que la compilación modifica el comportamiento en tiempo de ejecución, no los tipos. Además, ambos objetos se exportan, lo cual, como verá más adelante, tiene consecuencias.
Para la entrada, utilice dos cargas de trabajo que reflejen lo que realmente recibe su sistema. La válida aquí tiene tres elementos de línea, la moneda BDT y una clave de metadatos conocida. La inválida combina errores típicos de los usuarios: total: "na" (una celda de hoja de cálculo pegada en un campo numérico), un array lines vacío y email: "not-an-email".
Medición del tiempo de los tres analizadores
El conjunto de pruebas ejecuta 1,000 iteraciones en cada ruta para que el JIT se estabilice, luego mide 10,000 iteraciones, leyendo process.hrtime.bigint() antes y después de cada bucle, todo dentro del mismo proceso.
for (let i = 0; i < 10_000; i++) Invoice.safeParse(payload);
for (let i = 0; i < 10_000; i++) CompiledInvoice.parse(payload);
for (let i = 0; i < 10_000; i++) Invoice.validate(payload);
Hay dos precauciones antes de copiarlo. Primero, verifique qué forma ortográfica utiliza su versión para el camino booleano; el fragmento de código lo llama como un método en el esquema, mientras que el anuncio describe z.validate(). Si su instalación solo ofrece safeParse, entonces no está utilizando la versión 4.5, y un resultado de prueba de la versión 4.4 no debería presentarse como resultado de compilación. Segundo, una llamada a parse lanza un error con entrada inválida, por lo que al medir cargas de trabajo defectuosas, debe encerrar ese bucle en try/catch o utilizar la variante del analizador compilado que no lanza errores; de lo contrario, el bucle se detendrá con la primera carga inválida y la cifra resultante no tendrá sentido.
Reproducir la medición
Instale la última versión y confirme cuál es realmente la versión que tiene:
pnpm add zod@latest
pnpm exec node -e "import * as z from 'zod'; console.log('zod', z.VERSION ?? 'read package.json')"
Ponga el esquema en src/invoice.ts y los bucles en src/bench.ts. En Node 24, un archivo de TypeScript que utiliza únicamente sintaxis eliminable puede ejecutarse directamente gracias al procesamiento integrado de tipos:
node src/bench.ts
Si su código utiliza sintaxis que no puede eliminarse simplemente, ejecútelo con pnpm exec tsx src/bench.ts en su lugar. Sea cual sea la opción que elija, utilice el mismo ejecutor para los tres bucles y muestre los valores del reloj de alta resolución en lugar de depender únicamente de console.time, para que los números puedan compararse y analizarse.
Luego, sustituya el payload inválido y ejecute nuevamente. El resultado más interesante en 4.5 es el rendimiento con entrada defectuosa. La mayoría de las personas realizan pruebas de rendimiento solo con JSON válido, pero los endpoints públicos pasan mucho tiempo rechazando solicitudes mal formadas o malintencionadas.
La prueba funcional precede a la prueba de velocidad
Antes de comparar los relojes, confirme que cada analizador siga informando lo que necesita su interfaz de usuario. La verificación más sencilla es analizar la carga de datos defectuosa e imprimir la ruta del primer problema:
const a = Invoice.safeParse(bad);
console.log(a.success === false ? a.error.issues[0]?.path : "valid?");
Si el analizador compilado sigue devolviendo issues[0].path, un formulario que resalte el campo problemático puede pasar a él. Si lanza un error sin indicar una ruta, o solo devuelve un valor booleano, ya no podrá pasar el mismo mensaje a nivel de campo a useActionState ni a cualquier otro mecanismo de estado que muestre los errores del formulario.
Ese es el principal compromiso. Un valor booleano responde a la pregunta “¿Es esto realmente una factura?”, pero no puede indicar qué campo está incorrecto. Utilice la ruta booleana como umbral, por ejemplo delante de un proceso que procese filas CSV, y mantenga safeParse donde alguien necesite leer y corregir el error, como en un formulario que dibuje un contorno rojo alrededor del campo de correo electrónico.
Qué cambia y qué no cambia en la compilación
z.compile() no tiene nada que ver con el compilador de TypeScript. Conceptualmente, Zod genera un analizador más especializado para ese esquema en particular, de modo que el camino principal realice menos tareas genéricas e interpretativas. z.infer<typeof Invoice> no cambia; solo difiere el objeto en tiempo de ejecución al que se llama.
Dos objetos, no uno
El analizador compilado es una captura del esquema en el momento en que se compila. Los métodos de esquema de Zod, como .optional(), devuelven nuevos esquemas; por lo tanto, si más tarde se deriva una variante, por ejemplo haciendo que email sea opcional y se sigue utilizando CompiledInvoice, el analizador compilado seguirá rechazando los casos en que falte un correo electrónico. Ese es un comportamiento correcto, no un error: se están utilizando dos objetos independientes. Vuelva a compilar cada vez que cambie la definición del esquema y evite código que parezca modificar un esquema después de haberlo compilado.
Compilar una sola vez, al cargar el módulo
Compile en el ámbito del módulo, nunca dentro de un manejador de solicitudes. Compilar por solicitud genera exactamente el costo inicial que la función estaba diseñada para amortizar, y puede hacer que las operaciones sean más lentas que con safeParse simple.
Dónde importan los ahorros de memoria
La reducción del uso de memoria se debe a la forma en que las instancias de esquema almacenan sus métodos. Si su aplicación crea sus esquemas una sola vez al cargar el módulo, que es el patrón recomendado, es poco probable que observe un cambio drástico en la memoria del proceso. El ahorro es más significativo cuando existen un gran número de instancias de esquema, por lo que no espere que la cifra de 784 bytes aparezca en sus resultados.
Manténgalo en el servidor
La compilación es una optimización del lado del servidor. Colocar z.compile en un componente de cliente, por ejemplo en una página de configuración, añade trabajo y aumenta el tamaño del paquete para un navegador que, como máximo, analizará unas pocas cargas de datos.
Lectura de los resultados
Con este esquema de facturas, tras 10,000 iteraciones después del calentamiento en una sola laptop, una ejecución produjo estas cifras.
Cargas de datos válidas:
safeParse: 312 ms- Análisis compilado: 141 ms
Cargas inválidas:
safeParse: 498 ms- boolean validate: 39 ms
- compiled parse: 188 ms
Ninguno de estos valores alcanza 16 veces el tiempo necesario, excepto la operación booleana con entrada inválida que se acerca bastante a ese valor. Las mejoras siguen la dirección prometida en la versión actualizada, aunque son menores con esquemas más complejos. El valor más destacado, el de validación en datos inválidos, es también aquel que nunca se debe utilizar por sí solo detrás de un formulario.
Otras dimensiones merecen una línea cada una:
- Errores: en esta prueba,
safeParsecompilado siguió reportando un error relacionado con el correo electrónico inválido, por lo que el formulario pudo seguir utilizando esa información. - Forma de la API: el resultado de
z.compilees un valor separado que debe tratarse como una función fija e inmutable.
Límites de estas cifras
Estos resultados provienen de un único esquema, una sola máquina y 10,000 iteraciones de bucle después del período de calentamiento. La cifra de 16 veces se refiere a la validación en datos inválidos en el propio benchmark de los mantenedores; esta prueba mostró una gran mejora, no esa cifra destacada. La cantidad de memoria por cadena no se analizó de forma independiente. Además, un bucle estricto con hrtime mide el costo de la CPU, no la latencia percibida por el usuario: no dice nada sobre los Web Vitals de un visitante real en una red real.
Tus cargas de trabajo serán diferentes. Si tu esquema consiste en un único z.string(), compilarlo no ayudará a acelerar una solicitud lenta. Si se trata de un objeto anidado grande en una ruta con alto tráfico, mide el tiempo que tarda. Ejecuta 10,000 entradas válidas y 10,000 inválidas, y registra los tiempos junto con la primera ruta donde surgió un problema en las ejecuciones inválidas.
Un escenario de fallo del que aprender
Imagina una importación desde CSV que llamaba a Invoice.parse dentro de un bucle. Las filas válidas se procesaban rápidamente, pero las inválidas generaban estructuras completas de problemas, lo que retrasaba el proceso. Al cambiar la ruta de validación a una basada en booleanos se mejoró el rendimiento, pero ese mismo cambio también se aplicó a la ruta de la interfaz gráfica que debía resaltar el valor total. El resultado: un total de "na" generó un rechazo silencioso sin indicar el nombre del campo, y un valor incorrecto llegó a un informe a través de otra ruta que confiaba en los valores booleanos.
Un intento de solución que empeoró las cosas fue compilar el esquema dentro del manejador con cada solicitud; el costo de la configuración anuló cualquier beneficio obtenido.
Lo que realmente funcionó:
z.compileal cargar el módulo, utilizado por el proceso de importación;safeParseen el formulario, donde la persona puede leer los errores;- la ruta booleana validate únicamente como filtro antes de una cola.
La regla que sigue es: si un analizador compilado deja de reportar issues[0].path, el formulario no puede utilizarlo.
Registra tu entorno antes de realizar pruebas de rendimiento
Los resultados de rendimiento solo tienen sentido junto con las versiones que los generaron. En el proyecto de referencia, una pequeña aplicación Next.js con cuatro rutas relacionadas con facturas, la tecnología utilizada fue Node 24, TypeScript 7 y Next 16.3. Mantenga una nota de laboratorio en el repositorio, por ejemplo notes/lab.md, para que cada sesión parta de los mismos datos. Comience imprimiendo las versiones:
node -v
pnpm exec tsc -v
pnpm exec next --version
Escriba estas informaciones al principio de la nota. Si una versión importante difiere de la que asume la guía, deténgase y concilie la información antes de continuar, ya que los comandos posteriores pueden dar resultados plausibles pero engañosos.
A continuación, inicie el servidor de desarrollo y recorra las rutas para confirmar que la aplicación funciona normalmente antes de medir cualquier cosa:
pnpm exec next dev
Visite nuevamente /, /invoices, /invoices/1, /settings y /invoices, con la opción “Preservar registro” activada en DevTools, y capture el estado del cuadro de filtro junto con la URL.
Luego ejecute el comprobador de tipos e imprima su estado de salida:
pnpm exec tsc --noEmit --pretty false
echo $?
Un código de salida cero no demuestra que la aplicación funcione; solo significa que los tipos coinciden, y le permite pasar a las verificaciones en tiempo de ejecución. Después de eso, ejecute usted mismo los comandos de prueba de rendimiento de la sección de reproducción en lugar de confiar en las cifras de otra persona. El hardware, la temperatura ambiente, la presión de memoria y cualquier otro factor que opere en la máquina pueden afectar los tiempos de ejecución más que un simple lanzamiento de una biblioteca.
También es útil añadir en cada nota una línea de “solución fallida” de una sola oración, del tipo “se intentó X, pero aún se observó Y”. Esas líneas mantienen un registro honesto y evitan que se explore dos veces el mismo callejón sin salida.
Errores comunes
- Parser compilado en un componente de cliente. El paquete creció y las herramientas emitieron advertencias; la solución fue moverlo al worker.
- Validación booleana en un formulario. Devolvía
falsesin indicar la causa, lo que hacía que el usuario se quedara frente a un estado de error vacío e inútil; fue necesario restaurarsafeParse. - Cambiar el esquema después de compilarlo. El parser compilado seguía utilizando las reglas antiguas; en su lugar, hay que volver a compilarlo.
Lista de verificación antes de compilar un esquema
- Compilar al cargar el módulo, no por solicitud.
safeParse en los formularios y confirme que sigan leyendo issues[0].path.Mantenga los cinco tiempos de ejecución obtenidos en sus pruebas propias. Mencionar solo el mejor caso del proveedor no le dice nada sobre su sistema.
Dónde realmente sirve la compilación
En la aplicación de facturas, resultó que el importador CSV era el único lugar donde la compilación valía la pena. El formulario de configuraciones nunca lo necesitó, al igual que el webhook que recibe una factura a la vez. Compilarlos de todos modos causó otro problema: con dos objetos de esquema en memoria, un desarrollador asumió que el esquema exportado era “el compilado”, lo modificó, y el analizador en ejecución ignoró ese cambio. Esas compilaciones se deshicieron.
La política que se mantuvo es breve:
- compilar esquemas que se ejecutan dentro de bucles o en rutas realmente intensivas;
- no compilar para un único manejador POST;
- usar la verificación booleana como barrera de cola;
- usar
safeParsesiempre que alguien tenga que leer una ruta de error.
Escriba estas reglas como comentarios en la parte superior del archivo de esquema, donde permanecerán más allá de cualquier entrada de blog o hilo de chat:
// bench: pnpm exec tsx src/bench.ts
// expect: valid safeParse ~300ms / compiled ~140ms on 10k
// form: safeParse only
// worker loop: CompiledInvoice
// queue gate: Invoice.validate
Si el encabezado y el código no coinciden, uno de ellos está incorrecto, por lo que hay que corregirlo el mismo día. Un encabezado que indica “form: safeParse only” sobre un formulario que en realidad llama a validate es exactamente cómo ocurre el escenario de fallo mencionado anteriormente.
Para obtener una visión más amplia sobre el intercambio de esquemas entre cliente y servidor, consulte compartir un esquema Zod entre tu frontend React y backend Node.
Puntos clave
z.compile()sacrifica una configuración inicial única a cambio de un analizador en tiempo de ejecución más rápido; solo resulta beneficioso cuando el esquema se ejecuta muchas veces, por lo que debe compilarse al cargar el módulo en el servidor.- La ruta booleana validate es drásticamente más rápida con entrada inválida porque omite el árbol de problemas, y esa es también la razón por la cual nunca debe usarse con un formulario que necesite errores a nivel de campo.