Zod-Validierung beschleunigen mit kompilierten Schemas
Erfahren Sie, wie kompilierte Zod-Schemas die Validierung beschleunigen, wann sich der Gewinn lohnt und wie Sie Kosten, inkompatible Schemas und CSP-Einschränkungen berücksichtigen.
Seit Zod 4.5.0 wandelt z.compile() ein Schema in einen spezialisierten JavaScript-Validator um. Dieser prüft gültige Eingaben schneller und liefert exakt dieselben Ergebnisse und Fehler wie das unkompilierte Schema.
Wenn Ihr Handler denselben großen Request-Body tausende Male pro Minute parst, taucht eine Validierungsschicht, die bei jedem Aufruf den Schemabaum durchläuft, in Ihren CPU-Profilen auf. Mit dem Release von Zod 4.5 gibt es dafür eine Lösung. Dieser Artikel knüpft an den OpenReplay-Leitfaden zur Datenvalidierung in TypeScript mit Zod an. Er zeigt, wie Sie die Kompilierung aktivieren, welche Kosten sie verursacht, wo der Geschwindigkeitsgewinn tatsächlich ins Gewicht fällt und was unter einer strikten Content Security Policy oder auf Laufzeitumgebungen wie Cloudflare Workers passiert.
Das Wichtigste in Kürze
z.compile(schema)erzeugt einen Validator ohne Schleifen und ohne verzweigende Baumtraversierung und führt ihn übernew Function()aus. Gültige Eingaben werden durch eine schlichte Abfolge vontypeof-Prüfungen validiert, statt den Schemabaum Schritt für Schritt zu durchlaufen.- Ungültige Eingaben profitieren kaum, denn ein fehlgeschlagener Parse-Vorgang durchläuft zuerst den Fast Path und anschließend den vollständigen Standard-Parser, um die Fehler zu erzeugen.
- In Zods eigenem Tight-Loop-Benchmark laufen kompilierte Objekte mit 5, 10, 20 und 50 Keys 1,8-, 2,2-, 5,0- bzw. 10,2-mal schneller – kleine Schemas gewinnen also wenig.
- Der Compiler vergrößert jedes Bundle, das
z.compile()aufruft oderzod/compileimportiert, um etwa 7 KB (gzip-komprimiert). - Mit
{ strict: true }werfen nicht kompilierbare Schemas einenZodCompileAsyncErroroderZodCompileUnsupportedError, statt stillschweigend auf den Standard-Parser zurückzufallen.
Wie funktionieren kompilierte Zod-Schemas?
Ein kompiliertes Zod-Schema validiert Eingaben mit generiertem Code, statt den Schemabaum zu durchlaufen. Zod liest das gesamte Schema ein einziges Mal ein, erzeugt daraus ein kurzes Stück JavaScript ohne Schleifen und wandelt dieses mit new Function() in eine Funktion um. Ab diesem Zeitpunkt werden gültige Eingaben geprüft, indem jede Property gelesen und ihr typeof getestet wird – Zeile für Zeile. Lehnt der Fast Path eine Eingabe ab, führt Zod den Standard-Parser darauf aus. Deshalb meldet ein kompiliertes Schema dieselben Issues und Fehlermeldungen wie das ursprüngliche. Sie verwenden weiterhin .parse(), .safeParse() und dieselben inferierten Typen.
Kompilierung aktivieren
Es gibt zwei Möglichkeiten, die Kompilierung zu aktivieren: Sie kompilieren gezielt einzelne Schemas mit z.compile() oder kompilieren alle Schemas der Anwendung, indem Sie zod/compile global importieren. Die Kompilierung pro Schema gibt Ihnen präzise Kontrolle über Hot Paths. Der globale Modus erfordert nur eine einzige Zeile.
Pro Schema mit z.compile()
z.compile() gibt ein neues, kompiliertes Schema zurück. Das übergebene Schema bleibt unverändert. Jede Methode, die aus einem kompilierten Schema ein neues Schema erzeugt – etwa .refine(), .extend() oder .optional() –, liefert ein unkompiliertes Schema zurück. Stellen Sie daher zuerst das fertige Schema zusammen und kompilieren Sie es ganz zum Schluss:
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);
Zur Laufzeit weist nichts darauf hin, dass Wrong unkompiliert ist. Es validiert korrekt, nur langsamer. Auch die Option strict (siehe unten) erkennt diesen Fall nicht, da sie nur dann eine Exception wirft, wenn sich ein Schema überhaupt nicht kompilieren lässt. Die sichere Gewohnheit lautet daher: z.compile() ist immer der letzte Aufruf in der Kette.
Global mit zod/compile
Mit Compile-by-Default kompiliert Zod jedes Schema, das nach dem Import erstellt wird. Dies geschieht lazy, also beim ersten Parse-Vorgang des jeweiligen Schemas:
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
Da die Ladereihenfolge in ESM leicht falsch umgesetzt wird, sollten Sie das Modul besser direkt von der Laufzeitumgebung laden lassen:
node --import zod/compile app.js # ESM
node --require zod/compile app.cjs # CommonJS
Bun-Nutzer können zod/compile stattdessen unter preload in der bunfig.toml eintragen. Der globale Modus ist für Anwendungen gedacht. Bibliotheken sollten ihn nicht für ihre Konsumenten aktivieren.
Was kostet die Zod-Kompilierung?
Die Kompilierung von Zod-Schemas bringt drei Arten von Kosten mit sich und hilft bei ungültigen Eingaben kaum. Ein abgelehnter Parse-Vorgang durchläuft den Fast Path, scheitert und führt dann den vollständigen Standard-Parser aus, der nahezu die gesamte Zeit beansprucht. Die drei Kostenfaktoren sind:
- Einmaliger Kompilierungsaufwand pro Schema – beim Kompilieren des Schemas bzw. im globalen Modus beim ersten Parse-Vorgang.
- Bundle-Größe. Der Compiler fügt etwa 7 KB gzip-komprimiert (28 KB minifiziert) hinzu. Laut Zods eigener Tabelle wächst ein Objektschema mit vier Keys durch den Compiler von 24,1 KB auf 31,1 KB (gzip). Bei Zod Mini fällt der Sprung größer aus: von 4,6 KB auf 13,2 KB. Bundles, die weder
z.compile()aufrufen nochzod/compileimportieren, entfernen den Compiler beim Tree-Shaking vollständig. - Doppelte Ausführung bei Fehlern. Eine Refinement- oder Transform-Funktion wird bei gültiger Eingabe einmal ausgeführt, bei ungültiger Eingabe jedoch unter Umständen zweimal. Ein Refinement, das loggt, zählt oder schreibt, tut dies bei einem abgelehnten Webhook also doppelt.
An einer Request-Grenze mit kontinuierlichem Traffic amortisiert sich der einmalige Aufwand schnell. In einem einmaligen Skript oder einem CLI-Tool, das eine einzige Konfigurationsdatei parst, werden Kompilierungsaufwand und zusätzliche Bytes möglicherweise nie wieder hereingeholt. Auch Traffic, der überwiegend aus ungültigen Eingaben besteht – etwa Bot-Probes oder gefälschte Webhook-Signaturen –, profitiert kaum.
Wo zeigen sich die Performance-Gewinne von Zod?
Die Performance-Gewinne durch die Kompilierung wachsen mit der Größe des Schemas. Breite Objekte und Tupel profitieren am meisten, da der generierte Code jeden Key der Reihe nach prüft – ohne die Pro-Key-Schleife des Standard-Parsers. Zods eigener Benchmark misst jedes Schema isoliert und wiederholt in einer engen Schleife. In diesem Szenario schneidet der Standard-Parser am besten ab, daher fallen die Gewinne hier geringer aus als im Hauptdiagramm oben auf derselben Seite.
| Schema | Beschleunigung |
|---|---|
| Objekt, 5 Keys | 1,8x |
| Objekt, 10 Keys | 2,2x |
| Objekt, 20 Keys | 5,0x |
| Objekt, 50 Keys | 10,2x |
| Tupel, 1 Element | 2,2x |
| Tupel, 3 Elemente | 2,5x |
| Tupel, 5 Elemente | 3,0x |
| Tupel, 10 Elemente | 3,7x |
Ein Login-Formular mit drei Feldern wird keinen Unterschied bemerken. Ein Event-Payload mit 50 Keys, der bei jedem Request geparst wird, dagegen schon. Größere Zahlen wie das für zod-compiler genannte „bis zu 44x“ (bzw. bis zu 46x bei abgelehnten Eingaben) stammen von separaten Drittanbieter-Tools, die Validatoren zur Build-Zeit generieren. Sie beschreiben nicht den integrierten Laufzeit-Compiler von Zod.
Welche Zod-Schemas lassen sich nicht kompilieren?
Einige Features von Zod-Schemas lassen sich nicht kompilieren. Stößt z.compile() auf eines davon, wirft es keine Exception, sondern gibt stillschweigend das übergebene Schema unkompiliert zurück. Anhand der Liste nicht unterstützter Features lässt sich ablesen, ob das gesamte Schema oder nur ein einzelnes Kind-Element betroffen ist:
| Feature | Auswirkung |
|---|---|
| Asynchrone Refinements, Transforms oder Checks an beliebiger Stelle im Baum | Gesamtes Schema fällt auf den Standard-Parser zurück |
.catch() mit Callback (.catch(value) lässt sich kompilieren) | Gesamtes Schema fällt auf den Standard-Parser zurück |
| Union mit einem nicht unterstützten Member | Gesamtes Schema fällt auf den Standard-Parser zurück |
z.xor(), rekursive Schemas, z.coerce.*, Checks mit benutzerdefiniertem when | Nicht kompiliert |
| Nicht unterstütztes Kind-Element innerhalb eines Objekts, Arrays, Tupels, Records oder einer Intersection | Nur dieses Kind-Element nutzt den Standard-Parser |
Die Kompilierung greift nie bei z.encode() oder beim asynchronen Parsen. Beide laufen immer über den Standard-Parser. Wenn Ihre Handler safeParseAsync aufrufen, bringt die Kompilierung des Schemas ihnen also nichts.
Um zu verhindern, dass eine spätere Änderung die Kompilierung auf einem Hot Path unbemerkt deaktiviert, sollten Sie in der CI mit { strict: true } kompilieren:
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
});
Ist strict gesetzt, wirft ein asynchrones Schema einen ZodCompileAsyncError, und jedes andere Schema, das Zod nicht kompilieren kann, einen ZodCompileUnsupportedError. Ohne strict wird keiner der beiden Fehler geworfen.
Funktioniert z.compile() unter CSP und auf Cloudflare Workers?
Wo dynamische Codegenerierung verboten ist, schlägt z.compile() zwar sicher fehl, bringt aber keinerlei Nutzen. new Function() wird auf jeder Seite blockiert, deren Content Security Policy 'unsafe-eval' nicht in script-src aufführt (bzw. nicht in default-src, falls kein script-src definiert ist). Zod nennt außerdem Cloudflare Workers als Umgebung, in der new Function() blockiert ist. Wenn Sie jitless setzen, deaktiviert sich der globale Modus selbst:
// config.ts: import this module before any module that defines schemas
import * as z from "zod";
z.config({ jitless: true });
Ein expliziter Aufruf von z.compile() verhält sich anders. Zod wertet ihn als eindeutige Anforderung und versucht auch bei aktivem jitless, Code zu generieren. Blockiert die Umgebung new Function, erhalten Sie das Schema einfach unkompiliert zurück. Die Validierung funktioniert weiterhin. Allerdings liefern Sie dann rund 7 KB (gzip) Compiler-Code aus, der niemals ausgeführt werden kann. Lassen Sie zod/compile und z.compile() daher aus Builds für solche Umgebungen heraus.
Fazit
Die Kompilierung beschleunigt die Prüfung gültiger Eingaben, ohne Ergebnisse oder Fehler zu verändern – und der Gewinn wächst mit der Breite des Schemas. Beginnen Sie damit, Ihre Request-Grenze zu profilen. Kompilieren Sie Ihre breitesten und meistgenutzten Schemas als letzten Schritt ihrer Build-Kette und ergänzen Sie einen strict-Test, damit sie kompilierbar bleiben. Halten Sie den Compiler aus Builds für Laufzeitumgebungen und CSP-Richtlinien heraus, die new Function blockieren. Installieren Sie das jeweils aktuelle Zod-4-Release, statt auf Version 4.5.0 zu pinnen, damit Sie Fehlerbehebungen am Compiler mitnehmen.
FAQs
Gibt es in Zod eine schnellere Möglichkeit als safeParse, um ungültige Eingaben abzulehnen?
Ja. Zod 4.6 hat .validate() eingeführt, das lediglich zurückgibt, ob die Eingabe gültig ist. Die Methode verzichtet auf das Erzeugen eines ZodError, sodass das Abweisen ungültiger Eingaben kaum etwas kostet. Bei einem kompilierten Schema mit ungültiger Eingabe kann sie bis zu 35-mal schneller sein als .safeParse().success. Zudem verengt sie den Typ: Gibt sie true zurück, behandelt TypeScript den Wert als Input-Typ des Schemas. Verwenden Sie .validate(), wenn Sie nur eine Ja-/Nein-Antwort benötigen, und bleiben Sie bei .safeParse(), wenn Sie die Fehlerdetails brauchen.
Kann ich einen vorkompilierten Zod-Parser in Umgebungen verwenden, die new Function blockieren?
Ja, ab Zod 4.6. z.compile() erledigt zwei Aufgaben: Es generiert einen Parser und hängt ihn an das Schema an. z.withParser() übernimmt nur die zweite Aufgabe. Sie übergeben einen anderswo erzeugten Parser – etwa aus einem Build-Schritt oder von einem nativen Compiler –, und die Funktion hängt ihn nach denselben Regeln an, die auch z.compile() befolgt. Da der generierte Code als gewöhnliches JavaScript ausgeliefert wird, benötigt die Laufzeitumgebung niemals new Function.
Was ist der Unterschied zwischen z.compile() und zod-compiler?
z.compile() ist in Zod integriert und generiert Validatoren zur Laufzeit mit new Function() innerhalb Ihres Prozesses. zod-compiler (gajus/zod-compiler) ist ein separates Drittanbieter-Tool, das Validatoren zur Build-Zeit generiert – über Bundler-Plugins für Vite, webpack, esbuild, Rollup und weitere oder über eine CLI. Die Build-Zeit-Ausgabe ist reiner Code ohne eval, sodass CSP-Regeln sie nicht deaktivieren können. Der optionale Laufzeitmodus verwendet allerdings new Function. Das aktuelle Release von zod-compiler setzt Zod 4.5 oder neuer voraus; die 1.x-Linie deckt Zod 4.0 bis 4.4 ab.
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