Accueil / Articles

Zod 4.5 compile, valide et safeParse : choix d’un analyseur par limite

Comment évaluer les analyseurs compilés de Zod 4.5 ainsi que la voie rapide de validation booléenne sur des charges utiles valides et non valides, et déterminer à quelle API chacune appartient sans perdre les chemins d’erreur.

Éditeur : REACTAPP.TOP11 min de lecture2605 mots

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

Si un gestionnaire de route commence par schema.safeParse(body), vous payez le coût d’interprétation de Zod à chaque requête. La version 4.5 de Zod, annoncée le 28 août 2026, ajoute z.compile() afin que une partie de ce travail soit effectuée une seule fois, ainsi qu’un chemin rapide de validation booléenne qui évite la création de détails d’erreur. Une analyse plus rapide n’est utile que si les informations d’erreur dont dépend votre interface utilisateur restent intactes ; donc ce guide montre comment mesurer le temps de traitement des trois options sur un schéma réaliste, avec des entrées valides et invalides, ainsi que comment déterminer quelle API convient à chaque étape d’une application.

Ce que la version 4.5 prétend réellement

Il est utile de distinguer les déclarations officielles de la version des titres qui circulent par la suite. Selon l’annonce de la version 4.5 :

  • n’importe quel schéma peut être pré-compilé avec z.compile(schema), ce qui accélère l’analyse ;
  • z.validate() est une solution rapide de type booléen, qui serait jusqu’à 16 fois plus rapide avec des données invalides car elle ne construit pas l’arbre complet des problèmes ;
  • la mémoire conservée par schéma a fortement diminué : une simple fonction z.string() est passée d’environ 7,5 kB dans la version 4,4 à 784 octets dans la version 4,5.
  • Considérez ces valeurs comme des chiffres de référence fournis par les mainteneurs. L’expression « jusqu’à 16 fois » décrit le meilleur cas possible, il faut donc l’interpréter comme une tendance d’amélioration plutôt que comme un résultat à attendre. Cette interface API est nouvelle au moment de la rédaction de ce texte ; vérifiez donc les noms et les signatures dans la documentation actuelle de Zod ainsi que dans votre version installée avant de vous fier à quoi que ce soit ici présenté.

    Un schéma réaliste pour les tests

    Les micro-benchmarks sur une seule fonction z.string() ne vous apprennent pas grand-chose sur une application. Un test plus fiable utilise un schéma similaire au trafic réel : une facture avec un identifiant, une adresse e-mail, un montant total, une devise, une liste d’éléments et un enregistrement de métadonnées facultatif. Rien n’est simplifié pour faire bonne impression au compilateur.

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

    Remarquez trois points importants. CompiledInvoice est créé une seule fois, au niveau du module. Le type déduit provient toujours du schéma original Invoice, car la compilation modifie le comportement en temps de exécution, mais pas les types. De plus, ces deux objets sont exportés, ce qui, comme vous le verrez plus tard, a des conséquences.

    Pour l’entrée, utilisez deux ensembles de données qui reflètent ce que votre système reçoit réellement. Celui qui est valide contient trois éléments, la monnaie BDT et une clé de métadonnées connue. Celui qui est invalide combine des erreurs typiques des utilisateurs : total: "na" (une cellule de tableau collée dans un champ numérique), un tableau lines vide et email: "not-an-email".

    Temps d’exécution des trois analyseurs

    L’outil exécute 1 000 itérations pour chauffer chaque chemin afin que le JIT se soit stabilisé, puis mesure 10 000 itérations en lisant process.hrtime.bigint() avant et après chaque boucle, tout cela dans le même processus.

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

    Deux précautions avant de le copier. Premièrement, vérifiez quelle orthographe est utilisée pour la voie booléenne dans votre version ; l’extrait le considère comme une méthode sur le schéma, tandis que la description mentionne z.validate(). Si votre installation ne propose que safeParse, vous n’êtes pas sur la version 4.5, et un résultat de benchmark pour 4.4 ne devrait pas être présenté comme un résultat de compilation. Deuxièmement, une appel à parse génère une erreur en cas d’entrée invalide ; par conséquent, lorsque vous mesurez des données défectueuses, entourez cette boucle de try/catch ou utilisez la version sans erreur du parseur compilé ; sinon, la boucle s’arrête dès la première donnée invalide et le chiffre n’a plus de sens.

    Réaliser la mesure

    Installez la dernière version et confirmez celle que vous avez réellement obtenue :

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

    Placer le schéma dans src/invoice.ts et les boucles dans src/bench.ts. Sur Node 24, un fichier TypeScript utilisant uniquement une syntaxe érasable peut être exécuté directement grâce au débogage des types intégré :

    node src/bench.ts
    

    Si votre code utilise une syntaxe qui ne peut pas être simplement éradiquée, exécutez-le plutôt avec pnpm exec tsx src/bench.ts. Quelle que soit votre choix, utilisez le même outil d’exécution pour les trois boucles, et affichez les valeurs de l’horloge en haute résolution plutôt que de vous fier uniquement à console.time, afin que les chiffres puissent être comparés et discutés.

    Ensuite, remplacez le payload invalide et exécutez à nouveau. Le résultat le plus intéressant dans 4.5 concerne la performance avec des entrées défectueuses. La plupart des gens effectuent des tests de performance uniquement avec du JSON valide, mais les points d’accès publics passent beaucoup de temps à rejeter des requêtes mal formatées ou malveillantes.

    Le test fonctionnel précède le test de vitesse

    Au préalable de comparer les horloges, assurez-vous que chaque analyseur continue de fournir ce dont votre interface a besoin. La vérification la plus simple consiste à analyser le chargement défectueux et à afficher le chemin du premier problème :

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

    Si l’analyseur compilé renvoie toujours issues[0].path, un formulaire qui met en évidence le champ problématique peut se baser sur cette information. S’il lance une erreur sans indiquer de chemin, ou ne renvoie qu’un booléen, il devient impossible d’envoyer le même message au niveau du champ dans useActionState ou tout autre mécanisme de gestion des erreurs du formulaire.

    C’est là l’équilibre fondamental. Un booléen permet de répondre à la question « s’agit-il bien d’une facture ? », mais il ne permet pas de savoir quel champ est incorrect. Utilisez ce booléen comme filtre, par exemple avant un processus qui traite les lignes CSV, et conservez safeParse là où une personne doit lire et corriger l’erreur, comme dans un formulaire qui dessine un contour rouge autour du champ d’adresse e-mail.

    Que la compilation change et ne change pas

    z.compile() n’a rien à voir avec le compilateur TypeScript. Conceptuellement, Zod génère un analyseur plus spécialisé pour ce schéma particulier, de sorte que le chemin principal effectue moins de tâches génériques et interprétatives. z.infer<typeof Invoice> reste inchangé ; seul l’objet exécuté en temps de course diffère.

    Deux objets, pas un

    Le analyseur compilé représente un état instantané du schéma au moment de sa compilation. Les méthodes de schéma Zod telles que .optional() renvoient de nouveaux schémas ; par conséquent, si vous créez ultérieurement une variante en rendant par exemple email optionnel tout en continuant à utiliser CompiledInvoice, l’analyseur compilé refusera toujours un email manquant. Il s’agit d’un comportement normal et non d’une erreur : vous manipulez en effet deux objets indépendants. Recompilez chaque fois que la définition du schéma change, et évitez les codes qui semblent modifier un schéma après sa compilation.

    Compilerez une seule fois, au chargement du module

    Effectuez la compilation au niveau du module, jamais à l’intérieur d’un gestionnaire de requête. La compilation par requête entraîne exactement les coûts de mise en place prévus pour amortir ces frais, et elle peut facilement ralentir les opérations par rapport à safeParse classique.

    Où les économies de mémoire sont importantes

    La réduction de la mémoire est liée à la manière dont les instances de schéma stockent leurs méthodes. Si votre application crée ses schémas une seule fois au chargement du module, ce qui est le modèle recommandé, il est peu probable que vous observiez une variation significative de la mémoire utilisée par le processus. Les économies deviennent plus importantes lorsque le nombre d’instances de schéma est très élevé ; ne vous attendez donc pas à voir la valeur de 784 octets apparaître dans vos statistiques RSS.

    Gardez-le sur le serveur

    La compilation est une optimisation côté serveur. Placer z.compile dans un composant client, par exemple sur une page de paramètres, ajoute des tâches et alourdit le fichier à charger pour un navigateur qui ne traitera au maximum que quelques données.

    Lecture des résultats

    Avec ce schéma de facture, après 10 000 itérations sur un seul ordinateur portable, une seule exécution a donné les chiffres suivants.

    Données valides :

    • safeParse : 312 ms
    • parse compilé : 141 ms

    Chargements invalides :

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

    Aucun de ces résultats n’atteint 16 fois la valeur maximale, à l’exception du test boolean sur des données invalides qui s’en rapproche fortement. Les améliorations vont dans le sens promis lors de la sortie, mais en moindre mesure pour des schémas plus complexes. La valeur la plus remarquable, celle de validate sur des données invalides, est aussi celle qu’il ne faut absolument pas utiliser seule derrière un formulaire.

    Quelques autres aspects méritent une mention :

    • Erreurs : dans ce test, la version compilée de safeParse a encore signalé une erreur pour l’adresse e-mail invalide, ce qui permettait au formulaire de continuer à l’utiliser.
    • Forme de l’API : le résultat de z.compile est une valeur distincte qui doit être traitée comme une fonction immuable et fixe.
  • Bundel : le analyseur compilé ne doit pas se trouver dans une île côté client, comme celle qui filtre un tableau.
  • Charge cognitive : il existe désormais trois API d’analyse, il convient donc de définir une politique pour chaque limite et de l’indiquer à côté du schéma.
  • Limites de ces chiffres

    Ces résultats proviennent d’un seul schéma, d’une seule machine et de 10 000 itérations de boucle après un temps d’échauffement. Le chiffre 16x fait référence à la validation de données invalides dans le benchmark des mainteneurs ; ce test a montré une amélioration significative, mais pas ce chiffre en titre. La valeur du heap par chaîne n’a pas été mesurée de manière indépendante. De plus, une boucle hrtime stricte mesure le coût en CPU, et non la latence perçue par l’utilisateur : elle ne dit rien sur les Web Vitals pour un visiteur réel sur un réseau réel.

    Vos charges de travail seront différentes. Si votre schéma se compose d’un seul z.string(), sa compilation ne permettra pas d’accélérer une requête lente. S’il s’agit d’un objet imbriqué volumineux sur un chemin fréquentement utilisé, mesurez le temps nécessaire pour son traitement. Exécutez 10 000 entrées valides et 10 000 entrées invalides, puis enregistrez les temps d’exécution ainsi que le premier chemin où une erreur est survenue lors des entrées invalides.

    Un scénario d’échec à retenir

    Prenons l’exemple d’une importation CSV qui appelait Invoice.parse à l’intérieur d’une boucle. Les lignes valides étaient traitées rapidement, mais les lignes invalides généraient des arbres d’erreurs complets, ce qui ralentissait le traitement. Le passage du mécanisme de validation au chemin basé sur une valeur booléenne a permis d’améliorer la vitesse de traitement, mais cette même modification a également été appliquée au chemin de l’interface utilisateur censé mettre en évidence la valeur total. Résultat : une valeur totale de "na" a entraîné un rejet silencieux sans indication du nom du champ, et une valeur incorrecte a été incluse dans un rapport via un autre chemin qui faisait confiance à la valeur booléenne.

    Une tentative de correction qui a aggravé les choses consistait à compiler le schéma à l’intérieur du gestionnaire à chaque requête ; le coût de mise en place a annulé tout avantage.

    Ce qui a vraiment fonctionné :

    • z.compile au chargement du module, utilisé par le processus d’importation ;
    • safeParse sur le formulaire, où une personne lit l’erreur ;
    • le chemin booléen validate uniquement en tant que filtre avant une file d’attente.

    Règle suivante : si un analyseur compilé cesse un jour de signaler issues[0].path, le formulaire ne peut pas l’utiliser.

    Enregistrez votre environnement avant de réaliser des tests de performance

    Les résultats de performance n’ont de sens que par rapport aux versions qui les ont générés. Dans le projet de référence, une petite application Next.js comprenant quatre routes liées aux factures, l’ensemble des outils utilisés était Node 24, TypeScript 7 et Next 16.3. Conservez une note de laboratoire dans le répertoire, par exemple notes/lab.md, afin que chaque session parte des mêmes informations. Commencez par afficher les versions :

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

    Écrivez-les en haut de la note. Si une version majeure diffère de celle présumée dans le guide, arrêtez-vous et mettez les choses au clair avant de continuer, car des commandes ultérieures pourraient fournir des résultats plausibles mais trompeurs.

    Ensuite, lancez le serveur de développement et explorez les routes pour vous assurer que l’application fonctionne normalement avant de mesurer quoi que ce soit :

    pnpm exec next dev
    

    Visitez à nouveau /, /invoices, /invoices/1, /settings et /invoices, en activant l’option « Préserver le journal » dans les outils de développement, et enregistrez l’état du champ de filtrage ainsi que l’URL correspondante.

    Puis exécutez le vérificateur de types et affichez son code de sortie :

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

    Il est également utile d’ajouter à chaque note une ligne de « correction échouée » en une phrase, sous la forme « j’ai essayé X, mais Y est toujours présent ». Ces lignes assurent une description honnête des tentatives et empêchent d’explorer à nouveau le même cul-de-sac.

    Erreurs fréquentes

    • Parser compilé dans un composant client. Le bundle grossissait et les outils se plaignaient ; la solution a été de le déplacer dans le worker.
    • Vérification booléenne sur un formulaire. Elle renvoyait false sans indiquer de cause, laissant l’utilisateur face à un état d’erreur vide et inutile ; il a fallu rétablir safeParse.
    • Modification du schéma après compilation. Le parser compilé conservait les anciennes règles ; il faut plutôt recompiler.

    Liste de contrôle avant compilation d’un schéma

    • Compiler au chargement du module, et non à chaque demande.
  • Utilisez des analyseurs compilés uniquement dans les workers et les Route Handlers, jamais dans les composants clients.
  • Gardez safeParse sur les formulaires et assurez-vous qu’ils lisent toujours issues[0].path.
  • Utilisez la fonction booléenne validate path uniquement lorsque une réponse oui/non suffit.
  • Testez 10 000 charges de travail valides et 10 000 chargements invalides avec votre propre schéma.
  • Gardez les cinq temps de traitement obtenus lors de vos propres tests. Citer uniquement le meilleur résultat fourni par le fournisseur ne vous donne aucune information sur votre système.

    Où la compilation est réellement utile

    Dans l’application de facturation, l’importateur CSV s’est avéré être le seul endroit où la compilation était utile. Le formulaire de configuration n’en avait jamais eu besoin, tout comme le webhook qui reçoit une facture à la fois. La compilation de ces éléments a cependant provoqué un autre problème : avec deux objets de schéma en mémoire, un développeur a supposé que le schéma exporté était « celui compilé », l’a modifié, mais le parseur en cours d’exécution a ignoré ce changement. Ces compilations ont donc été annulées.

    La politique à suivre est concise :

    • compiliez les schémas qui sont exécutés à l’intérieur de boucles ou sur des chemins vraiment fréquents ;
    • ne compilez pas pour un seul gestionnaire POST ;
    • utilisez la vérification booléenne comme porte d’entrée dans la file d’attente ;
    • utilisez safeParse chaque fois qu’une personne doit lire un chemin d’erreur.

    Écrivez ces règles sous forme de commentaires en haut du fichier de schéma, afin qu’elles survivent à tout article de blog ou thread de discussion :

    // 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 l’en-tête et le code ne coïncident pas, l’un d’eux est incorrect, il faut donc le corriger le même jour. Un en-tête indiquant "form: safeParse only" au-dessus d’un formulaire qui appelle réellement la fonction validate est précisément ce qui provoque le scénario d’échec décrit ci-dessus.

    Pour en savoir plus sur le partage de schémas entre le client et le serveur, consultez le partage d’un schéma Zod entre votre interface React et votre backend Node.

    Points clés

    • z.compile() permet d’échanger une configuration initiale contre un analyseur en temps de exécution plus rapide ; cela ne s’avère avantageux que lorsque le schéma est exécuté de nombreuses fois, il convient donc de le compiler au chargement du module sur le serveur.
    • Le chemin booléen validate est considérablement plus rapide avec des données incorrectes, car il saute l’arbre des problèmes, ce qui explique également pourquoi il ne doit jamais être utilisé pour un formulaire nécessitant des erreurs au niveau des champs.
  • Tenez toujours des tests de référence pour les charges utiles invalides ainsi que pour celles valides, et vérifiez les chemins d’erreur avant de comparer les vitesses.