Startseite / Artikel

Zod 4.5: Kompilieren, Validieren und safeParse – Auswahl eines Parsers pro Grenze

Wie man die kompilierten Parser von Zod 4.5 sowie den schnellen Prüfweg für boolesche Werte an gültigen und ungültigen Payloads vergleicht, und wo jede API gehört, ohne die Fehlerwege zu verlieren.

Verlag: REACTAPP.TOP11 Min. Lesezeit2605 Wörter

Quellen: Zod 4.5 z.compile() Cut My Parse Time. Invalid Payloads Got Faster Than Valid Ones.

Falls ein Route-Handler mit schema.safeParse(body) beginnt, müssen Sie bei jeder Anfrage die Interpretationskosten von Zod tragen. Zod 4.5, das am 28. August 2026 veröffentlicht wurde, fügt z.compile() hinzu, sodass ein Teil dieser Arbeit nur einmal durchgeführt wird, sowie einen booleschen Schnellweg für die Validierung, der das Erstellen von Fehlerdetails überspringt. Eine schnellere Parsing-Verarbeitung ist nur nützlich, wenn die Fehlerinformationen, auf denen Ihre Benutzeroberfläche angewiesen ist, erhalten bleiben. Daher zeigt dieser Leitfaden, wie man die drei Optionen anhand eines realistischen Schemas mit guter und fehlerhafter Eingabe zeitlich misst, sowie wie man entscheidet, welche API an welcher Stelle einer Anwendung verwendet werden sollte.

Was die Version 4.5 tatsächlich verspricht

Es ist hilfreich, die eigenen Aussagen der Veröffentlichung von den später verbreiteten Schlagzeilen zu trennen. Laut der Ankündigung von 4.5:

  • Jedes Schema kann mit z.compile(schema) im Voraus kompiliert werden, was die Parsing-Geschwindigkeit erhöht;
  • z.validate() ist ein boolescher Schnellweg, der bei ungültigen Daten bis zu 16 Mal schneller ist, da er nicht den vollständigen Fehlerbaum erstellt;
  • der in jedem Schema behaltene Speicherplatz sank stark; eine einfache z.string() hatte von etwa 7,5 kB im Version 4.4 nur noch 784 Bytes in Version 4.5.
  • Betrachten Sie diese Werte als Vergleichszahlen der Entwickler. „Bis zu 16x“ beschreibt den besten Fall, weshalb man dies eher als Richtung der Verbesserung denn als das zu erwartende Ergebnis verstehen sollte. Diese API-Oberfläche ist zum Zeitpunkt der Erstellung neu, daher überprüfen Sie Namen und Signaturen unbedingt in der aktuellen Zod-Dokumentation sowie in Ihrer installierten Version, bevor Sie auf die hier angegebenen Informationen vertrauen.

    Ein realistisches Schema zum Testen

    Mikro-Benchmarks mit nur einer z.string()-Aufrufung liefern wenig Informationen über eine Anwendung. Ein ehrlicherer Test verwendet ein Schema, das dem tatsächlichen Datenverkehr entspricht: eine Rechnung mit einer ID, einer E-Mail-Adresse, einem Gesamtbetrag, einer Währung, einer Liste von Posten sowie optionalen Metadaten. Nichts wird vereinfacht, nur um dem Compiler ein besseres Bild zu vermitteln.

    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>;
    

    Beachten Sie drei Dinge. CompiledInvoice wird einmalig im Modulbereich erstellt. Die abgeleitete Typeninformation stammt weiterhin vom ursprünglichen Invoice-Schema, denn die Kompilierung ändert das Laufzeitverhalten nicht, sondern nur die Typen. Zudem werden beide Objekte exportiert, was, wie Sie später sehen werden, Konsequenzen hat.

    Zum Eingabedaten verwenden Sie zwei Payloads, die widerspiegeln, was Ihr System tatsächlich erhält. Der gültige Payload enthält drei Zeileinträge, die Währung BDT sowie einen bekannten Metadaten-Schlüssel. Der ungültige Payload weist typische Benutzerfehler auf: total: „na“ (ein Tabellenzelleninhalt, der in ein Zahlfeld eingefügt wurde), ein leeres lines-Array sowie email: „not-an-email“.

    Zeitmessung der drei Parser

    Das Testframework erhitzt jeden Pfad durch 1.000 Iterationen, damit sich der JIT eingestellt hat, misst anschließend 10.000 Iterationen und liest vor und nach jeder Schleife process.hrtime.bigint() aus – alles innerhalb desselben Prozesses.

    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);
    

    Zwei Hinweise, bevor Sie es kopieren. Erstens prüfen Sie, welche Schreibweise Ihre Version für den booleschen Pfad verwendet; der Codeausschnitt ruft diese Funktion als Methode auf dem Schema auf, während die Ankündigung z.validate() beschreibt. Wenn Ihre Installation nur safeParse anbietet, sind Sie nicht auf Version 4.5, und ein Benchmark für 4.4 sollte nicht als Kompilierergebnis dargestellt werden. Zweitens wirft ein parse-Aufruf bei ungültigen Eingaben einen Fehler aus, daher sollten Sie beim Messen fehlerhafter Daten entweder die Schleife in try/catch einschließen oder die fehlerfreie Variante des kompilierten Parsers verwenden; andernfalls stoppt die Schleife bereits bei der ersten fehlerhaften Datenmenge, wodurch die Zahl bedeutungslos wird.

    Wiederholung der Messung

    Installieren Sie die neueste Version und überprüfen Sie, welche Version tatsächlich installiert wurde:

    pnpm add zod@latest
    pnpm exec node -e "import * as z from 'zod'; console.log('zod', z.VERSION ?? 'read package.json')"
    

    Platzieren Sie das Schema in src/invoice.ts und die Schleifen in src/bench.ts. Unter Node 24 kann eine TypeScript-Datei, die nur lösbare Syntax verwendet, dank der eingebauten Typentfernung direkt ausgeführt werden:

    node src/bench.ts
    

    Falls Ihr Code Syntax verwendet, die nicht einfach entfernt werden kann, führen Sie ihn stattdessen mit pnpm exec tsx src/bench.ts aus. Unabhängig von der gewählten Methode sollten Sie für alle drei Schleifen denselben Ausführer verwenden und die Werte der Hochauflösungsuhr anzeigen, anstatt sich ausschließlich auf console.time zu verlassen, damit die Zahlen verglichen und diskutiert werden können.

    Wechseln Sie anschließend zum ungültigen Payload und führen Sie erneut aus. Das interessanteste Ergebnis in 4.5 ist die Durchsatzleistung bei schlechten Eingaben. Die meisten Menschen führen nur Tests mit gültigem JSON durch, doch öffentliche Endpunkte verbringen viel Zeit damit, fehlerhafte oder bösartige Anfragen abzulehnen.

    Der Funktionstest kommt vor dem Geschwindigkeitstest

    Vor dem Vergleich der Uhren sollten Sie sicherstellen, dass jeder Parser weiterhin das liefert, was Ihre Benutzeroberfläche benötigt. Die einfachste Überprüfung besteht darin, die fehlerhafte Datenmenge zu parsen und den Pfad des ersten Problems auszugeben:

    const a = Invoice.safeParse(bad);
    console.log(a.success === false ? a.error.issues[0]?.path : "valid?");
    

    Falls der kompilierte Parser weiterhin issues[0].path zurückgibt, kann ein Formular, das das fehlerhafte Feld hervorhebt, darauf umschalten. Wenn der Parser ohne Pfad einen Fehler auslöst oder nur ein Boolesches Wert zurückgibt, können Sie die gleiche Feldbeschreibung nicht mehr an useActionState oder irgendein anderes Mechanismus zur Anzeige von Formularfehlern weitergeben.

    Das ist der zentrale Kompromiss: Ein Boolescher Wert beantwortet die Frage „Ist das überhaupt eine Rechnung?“, kann aber nicht angeben, welches Feld fehlerhaft ist. Verwenden Sie den booleschen Pfad als Schleuse – beispielsweise vor einem Prozess, der CSV-Zeilen verarbeitet – und behalten Sie safeParse bei, wo eine Person den Fehler lesen und beheben muss, wie zum Beispiel in einem Formular, das einen roten Rahmen um das E-Mail-Eingabefeld zeichnet.

    Was die Kompilierung verändert und was nicht

    z.compile() hat nichts mit dem TypeScript-Kompilator zu tun. Konzeptionell erzeugt Zod einen spezielleren Parser für dieses bestimmte Schema, sodass der Hauptweg weniger generische, interpretative Aufgaben ausführt. z.infer<typeof Invoice> bleibt unverändert; nur das zur Laufzeit aufgerufene Objekt unterscheidet sich.

    Zwei Objekte, nicht eines

    Der kompilierte Parser ist ein Abbild des Schemas zum Zeitpunkt seiner Kompilierung. Zod-Schema-Methoden wie .optional() liefern neue Schemata zurück; daher wird der kompilierte Parser weiterhin ein fehlendes E-Mail-Field ablehnen, falls Sie später eine Variante erstellen, beispielsweise indem Sie email optional machen, und weiterhin CompiledInvoice verwenden. Das ist ein korrektes Verhalten und kein Bug – Sie arbeiten mit zwei unabhängigen Objekten. Kompilieren Sie erneut, sobald sich die Schema-Definition ändert, und vermeiden Sie Code, der den Eindruck erweckt, das Schema nach der Kompilierung zu ändern.

    Kompileieren Sie einmal, beim Laden des Moduls

    Kompilieren Sie im Modulbereich, niemals innerhalb eines Anfragenverarbeiters. Die Kompilierung pro Anfrage verursacht genau die Initialisierungskosten, die ursprünglich dafür gedacht waren, aufgeteilt zu werden, und kann die Leistung leicht schlechter machen als bei einer einfachen Verwendung von safeParse.

    Wo die Speichereinsparungen wichtig sind

    Die Verringerung des Speicherverbrauchs hängt davon ab, wie die Schema-Instanzen ihre Methoden speichern. Wenn Ihre Anwendung ihre Schemata beim Laden des Moduls einmal erstellt – was das empfohlene Vorgehen ist – werden Sie wahrscheinlich keine deutlichen Veränderungen im Prozessspeicher feststellen. Die Einsparungen sind erst dann von Bedeutung, wenn es sehr viele Schema-Instanzen gibt; erwarten Sie daher nicht, dass die Zahl von 784 Bytes in Ihren RSS-Daten erscheint.

    Lassen Sie es auf dem Server

    Kompilierung ist eine Optimierung auf Serverseite. Die Platzierung von z.compile in einer Client-Komponente, beispielsweise auf einer Einstellungsseite, verursacht zusätzliche Arbeit und erhöht das Paketgewicht für einen Browser, der höchstens einige wenige Datenpakete verarbeiten muss.

    Auswertung der Ergebnisse

    Bei diesem Rechnungs-Schema, nach 10.000 Iterationen nach dem Aufwärmen auf einem einzelnen Laptop, ergaben sich bei einer Ausführung folgende Werte.

    Gültige Datenpakete:

    • safeParse: 312 ms
    • kompilierte Verarbeitung: 141 ms

    Ungültige Payloads:

    • safeParse: 498 ms
    • boolean validate: 39 ms
    • compiled parse: 188 ms

    Niemand von diesen erreicht das 16-fache, außer dass der boolean-Wert bei ungültigen Eingaben dem Zielwert nahekommt. Die Verbesserungen gehen in die Richtung, die die Veröffentlichung versprochen hat, sind bei komplexeren Schemata jedoch geringer. Der auffälligste Wert, validate bei ungültigen Daten, ist auch der, den man auf keinen Fall allein hinter einem Formular verwenden sollte.

    Mehrere weitere Aspekte verdienen jeweils einen eigenen Eintrag:

    • Fehler: In diesem Test meldete die kompilierte Version von safeParse weiterhin einen Fehler für die fehlerhafte E-Mail-Adresse, sodass das Formular diese weiterhin verwenden konnte.
    • API-Struktur: Das Ergebnis von z.compile ist ein separater Wert, der als unveränderliche, feste Funktion behandelt werden sollte.
  • Paket: Der kompilierte Parser gehört nicht auf eine Client-Seite, die beispielsweise Tabellen filtert.
  • Kognitive Last: Es gibt nun drei Parsing-APIs, daher sollte man für jede Grenze eine Richtlinie festlegen und diese neben dem Schema notieren.
  • Beschränkungen dieser Zahlen

    Diese Ergebnisse stammen von einem Schema, einer Maschine und 10.000 Schleifeniterationen nach dem Aufwärmen. Die Zahl 16x bezieht sich auf die Überprüfung von ungültigen Daten im eigenen Benchmark der Betreuer; dieser Test zeigte eine große Verbesserung, nicht jedoch diesen Schlagzeilenwert. Die Angabe zum Heap pro Zeichenstrang wurde nicht unabhängig getestet. Zudem misst eine enge hrtime-Schleife den CPU-Aufwand, nicht die für den Benutzer sichtbare Latenz – sie sagt nichts über Web Vitals bei echten Besuchern in einem echten Netzwerk aus.

    Ihre Payloads werden unterschiedlich sein. Wenn Ihr Schema aus einem einzigen z.string() besteht, rettet das Kompilieren keinen langsamen Anfragenablauf. Ist es hingegen ein großes, verschachteltes Objekt auf einer stark genutzten Route, sollten Sie die Ausführungszeit messen. Führen Sie 10.000 gültige und 10.000 ungültige Eingaben durch und notieren Sie die Zeiten sowie den ersten Fehlerpfad bei den ungültigen Eingaben.

    Ein Scheiternsszenario, das man sich aneignen sollte

    Betrachten Sie einen CSV-Import, der innerhalb einer Schleife Invoice.parse aufruft. Gültige Zeilen wurden schnell verarbeitet, während ungültige Zeilen vollständige Fehlerstrukturen erzeugten und dadurch die Verarbeitung ins Stocken geriet. Der Wechsel auf den booleschen Validierungspfad verbesserte die Durchsatzrate, doch derselbe Änderungsweg wurde auch auf den UI-Pfad angewandt, der eigentlich den total-Wert hervorheben sollte. Das Ergebnis: Ein "na"-Wert führte zu einer stillen Ablehnung ohne Angabe des Feldnamens, und ein fehlerhafter Wert gelangte über einen anderen Pfad, der auf den booleschen Wert vertraute, in den Bericht.

    Eine versuchte Lösung, die die Situation verschlimmerte, bestand darin, das Schema bei jeder Anfrage innerhalb des Handlers zu kompilieren; die damit verbundenen Aufwandskosten überwogen den Nutzen.

    Was tatsächlich funktionierte:

    • z.compile bei der Moduleladung, das vom Import-Worker verwendet wird;
    • safeParse auf dem Formular, wo eine Person die Fehler liest;
    • der boolesche Validierungspfad lediglich als Schleusenfunktion vor einer Warteschlange.

    Die folgende Regel: Wenn ein kompiliertes Parser aufhört, issues[0].path zu melden, darf das Formular es nicht verwenden.

    Erstellen Sie eine Aufnahme Ihres Umfelds, bevor Sie Benchmarks durchführen

    Die Leistungsergebnisse sind nur im Zusammenhang mit den Versionen, aus denen sie stammen, aussagekräftig. Im Referenzprojekt, einer kleinen Next.js-App mit vier rechnungsbasierten Routen, bestand die Technologiestack aus Node 24, TypeScript 7 und Next 16.3. Führen Sie eine Labortagebuch-Eintragung im Repository, beispielsweise in notes/lab.md, damit jede Sitzung von denselben Informationen ausgeht. Beginnen Sie damit, die Versionen auszudrucken:

    node -v
    pnpm exec tsc -v
    pnpm exec next --version
    

    Schreiben Sie diese am Anfang des Tagebucheintrags auf. Wenn eine Hauptversion von der in einer Anleitung angenommenen abweicht, stoppen Sie und klären Sie die Abweichung auf, bevor Sie fortfahren – schließlich können spätere Befehle plausibel erscheinende, aber irreführende Ergebnisse liefern.

    Anschließend starten Sie den Entwicklungsserver und prüfen Sie die Routen, um sicherzustellen, dass die App normal funktioniert, bevor Sie Messungen durchführen:

    pnpm exec next dev
    

    Besuchen Sie erneut /, /invoices, /invoices/1, /settings und /invoices, wobei in den DevTools „Preserve log“ aktiviert sein soll, und erfassen Sie den Zustand des Filterfeldes zusammen mit der URL.

    Führen Sie anschließend den Typprüfer aus und geben Sie seinen Abbruchstatus aus:

    pnpm exec tsc --noEmit --pretty false
    echo $?
    

    Ein Abbruchcode von null ist kein Beweis dafür, dass die Anwendung funktioniert; er bedeutet lediglich, dass die Typen übereinstimmen, und gibt Ihnen die Erlaubnis, zu Laufzeitprüfungen überzugehen. Danach sollten Sie die Benchmark-Befehle aus dem Reproduktionsabschnitt selbst ausführen, anstatt den Werten anderer zu vertrauen. Hardware, Umgebungstemperatur, Speicherdruck sowie alle anderen Faktoren, die auf dem Rechner laufen, können die Laufzeiten stärker verändern als eine kleine Bibliotheksaktualisierung.

    Auch lohnt es sich, zu jeder Notiz eine einzeilige „fehlgeschlagene Korrektur“-Zeile hinzuzufügen, in der Form „X ausprobiert, immer noch Y“. Solche Zeilen sorgen dafür, dass die Aufzeichnungen genau sind, und verhindern, dass derselbe Sackgassenzustand zweimal untersucht wird.

    Häufige Fehler

    • Kompilierte Parser in einem Client-Komponenten. Das Bundle wuchs und die Tools meldeten Probleme; die Lösung bestand darin, den Parser in den Worker zu verlegen.
    • Boolesche Überprüfung in einem Formular. Sie gab false ohne Angabe eines Pfades zurück, wodurch der Benutzer mit einem nutzlosen, leeren Fehlerbild konfrontiert wurde; safeParse musste wieder eingeführt werden.
    • Anderung des Schemas nach der Kompilierung. Der kompilierte Parser behielt die alten Regeln bei; kompilieren Sie stattdessen erneut.

    Kontrollliste vor der Kompilierung eines Schemas

    • Kompilieren Sie beim Laden des Moduls, nicht pro Anfrage.
  • Verwenden Sie kompilierte Parser nur in Workers und Route-Handlers, niemals in Client-Komponenten.
  • Bewahren Sie safeParse bei Formularen auf und überprüfen Sie, ob diese weiterhin issues[0].path lesen.
  • Verwenden Sie die boolesche Funktion zum Prüfen des Pfads nur dort, wo eine Ja/Nein-Antwort ausreicht.
  • Testen Sie 10.000 gültige und 10.000 ungültige Payloads mit Ihrem eigenen Schema.
  • Bewahren Sie alle fünf Messwerte aus Ihrem eigenen Testlauf auf. Wenn nur der beste Fall des Anbieters angegeben wird, sagt das nichts über Ihr System aus.

    Wo die Kompilierung tatsächlich ihren Nutzen zeigt

    In der Rechnungs-App erwies sich der CSV-Importeur als der einzige Ort, an dem die Kompilierung sinnvoll war. Das Einstellungsformular benötigte sie nie, genauso wenig wie der webhook, der jeweils nur eine Rechnung erhält. Dennoch führte die Kompilierung dort zu einem anderen Problem: Da zwei Schema-Objekte im Speicher vorhanden waren, ging ein Entwickler davon aus, dass das exportierte Schema „das kompilierte“ sei, änderte es – doch der laufende Parser ignorierte die Änderung. Diese Kompilierungen wurden rückgängig gemacht.

    Die geltende Richtlinie ist kurz:

    • Kompilieren Sie Schemata, die innerhalb von Schleifen oder auf wirklich häufig genutzten Pfaden ausgeführt werden;
    • Kompilieren Sie nicht für einen einzelnen POST-Handler;
    • Verwenden Sie die boolesche Überprüfung als Schleusentür;
    • Verwenden Sie safeParse, wo immer jemand einen Fehlerpfad lesen muss.

    Schreiben Sie diese Regeln als Kommentare ganz oben im Schema-Datei, wo sie länger bestehen bleiben werden als jeder Blogbeitrag oder Chat-Thread:

    // 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
    

    Falls der Header und der Code nicht übereinstimmen, ist einer von ihnen falsch – korrigieren Sie ihn daher noch am selben Tag. Ein Header, der „form: safeParse only“ angibt, über einem Formular, das tatsächlich die Methode validate aufruft, führt genau zu dem oben beschriebenen Fehlerfall.

    Zum umfassenderen Überblick über den Austausch von Schemata zwischen Client und Server siehe den Artikel über den Austausch eines Zod-Schemas zwischen Ihrem React-Frontend und Node-Backend.

    Kernpunkte

    • z.compile() opfert eine einmalige Konfiguration für einen schnelleren Laufzeit-Parser ein; er lohnt sich nur, wenn das Schema häufig ausgeführt wird – daher sollte er bei der Module-Ladung auf dem Server kompiliert werden.
    • Der boolesche Pfad validate ist bei fehlerhaften Eingaben deutlich schneller, da er den Fehlerbaum überspringt – genau deshalb darf er niemals ein Formular unterstützen, das fehlerbehaftete Felder benötigt.
  • Prüfen Sie stets sowohl ungültige als auch gültige Payloads und überprüfen Sie die Fehlerwege, bevor Sie Geschwindigkeiten vergleichen.
  • Ein kompiliierter Parser ist ein eigenständiger Zustandsbild; rekompilieren Sie ihn, wenn sich das Schema ändert.
  • Betrachten Sie die Faktoren der Anbieter als Richtwerte, dokumentieren Sie Ihre Versionen und schreiben Sie Ihre Parsing-Regeln neben dem Schema.