Zod 4.5 kompilacja, walidacja i safeParse: Wybór parsera według granic
Jak przeprowadzić testy wydajności skompilowanych parserów Zod 4.5 oraz szybkiej ścieżki walidacji logicznej na poprawnych i niepoprawnych danych wejściowych, oraz gdzie znajduje się każda API, bez utraty ścieżek błędów.
Źródła: Zod 4.5 z.compile() Cut My Parse Time. Invalid Payloads Got Faster Than Valid Ones.
Jeśli obsługa trasy rozpoczyna się od schema.safeParse(body), za każdą prośbę płacisz koszt interpretacji zapewniany przez Zod. Wersja Zod 4.5, ogłoszona 28 sierpnia 2026 roku, wprowadza funkcję z.compile(), dzięki której część tych operacji wykonywana jest tylko raz, a także szybką ścieżkę walidacji logicznej omijającą generowanie szczegółów błędów. Szybsze parsowanie jest przydatne tylko wtedy, gdy informacje o błędach, od których zależy interfejs użytkownika, zostaną zachowane; dlatego ten przewodnik pokazuje, jak zmierzyć czas wykonywania trzech opcji na rzeczywistym schemacie, z zarówno poprawnymi, jak i uszkodzonymi danymi wejściowymi, oraz jak zdecydować, która API powinna być używana na każdej granicy aplikacji.
Co faktycznie obiecuje wersja 4.5
Pomaga oddzielić oficjalne oświadczenia dotyczące tej wersji od doniesień, które pojawiają się później. Zgodnie z ogłoszeniem o wersji 4.5:
- każdy schemat może zostać uprzednio skompilowany za pomocą
z.compile(schema), co przyspiesza parsowanie;
z.validate() to szybka ścieżka logiczna typu boolean, która jest nawet 16 razy szybsza przy nieprawidłowych danych, ponieważ nie tworzy pełnego drzewa problemów;z.string() przechodził z około 7,5 kB w wersji 4.4 na 784 bajty w wersji 4.5.Traktuj te wartości jako wskaźniki dostarczone przez twórców. „Aż 16 razy” odnosi się do najlepszego przypadku, więc należy to postrzegać jako kierunek ulepszeń, a nie jako wynik, którego należy się spodziewać. Ta interfejs API jest nowa w momencie pisania tego tekstu, dlatego przed poleganiem na podanych informacjach sprawdź nazwy i specyfikacje w aktualnej dokumentacji Zod oraz w swojej zainstalowanej wersji.
Rzeczywisty schemat do testowania
Mikropomiary wykonywane na pojedynczym z.string() niewiele mówią o aplikacji. Wierniejszy test wykorzystuje schemat przypominający rzeczywisty ruch danych: fakturę z identyfikatorem, adresem e-mail, kwotą łączną, walutą, listą pozycji oraz opcjonalnym rekordem metadanych. Nic nie jest uproszczane po to, by kompilator wyglądał lepiej.
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>;
Zwróć uwagę na trzy rzeczy. Obiekt CompiledInvoice jest tworzony tylko raz, w zakresie modułu. Typ wywnioskowany nadal pochodzi od oryginalnego schematu Invoice, ponieważ kompilacja zmienia zachowanie w czasie wykonywania, a nie typy. Oba obiekty są eksportowane, co, jak zobaczysz później, ma swoje konsekwencje.
Jako dane wejściowe użyj dwóch pakietów danych, które odzwierciedlają to, co faktycznie otrzymuje twój system. Poprawny przykład zawiera trzy pozycje, walutę BDT oraz znany klucz metadanych. Błędny przykład łączy typowe błędy użytkowników: total: „na” (komórka z arkusza kalkulacyjnego wklejona do pola numerycznego), pusty tabliczka lines oraz email: „not-an-email”.
Mierzenie czasu działania trzech parserów
Narzędzie przeprowadza 1 000 iteracji dla każdej ścieżki, aby JIT mogło się ustabilizować, a następnie mierzy 10 000 iteracji, czytając wartość process.hrtime.bigint() przed i po każdej pętli, wszystko to w tym samym procesie.
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);
Dwie uwagi przed skopiowaniem. Po pierwsze, sprawdź, jaką pisownię używa twoja wersja dla ścieżki booleanowej; fragment kodu odwołuje się do niej jako do metody w schemacie, podczas gdy ogłoszenie opisuje z.validate(). Jeśli twoja instalacja oferuje tylko safeParse, to nie masz wersji 4.5, a wynik z wersji 4.4 nie powinien być przedstawiany jako rezultat kompilacji. Po drugie, wywołanie parse rzuci błędem przy nieważnych danych, więc mierząc czas przetwarzania uszkodzonych danych, musisz albo otoczyć ten pętlę instrukcjami try/catch, albo użyć wersji parsera pozbawionej rzucania błędów; w przeciwnym razie pętla zatrzyma się przy pierwszych nieprawidłowych danych, a uzyskany wynik będzie bezsensowny.
Odtworzenie pomiaru
Zainstaluj najnowszą wersję i potwierdź, jaką wersję faktycznie masz:
pnpm add zod@latest
pnpm exec node -e "import * as z from 'zod'; console.log('zod', z.VERSION ?? 'read package.json')"
Umieść schemat w pliku src/invoice.ts, a pętle w pliku src/bench.ts. W Node 24 plik TypeScript używający wyłącznie składni, którą można usunąć, może być uruchomiony bezpośrednio dzięki wbudowanemu usuwaniu informacji typów:
node src/bench.ts
Jeśli twój kod wykorzystuje składnię, której nie można tak po prostu usunąć, uruchom go zamiast tego za pomocą polecenia pnpm exec tsx src/bench.ts. Niezależnie od wyboru, użyj tego samego narzędzia do uruchamiania wszystkich trzech pętli oraz wyświetl wartości zegara o wysokiej rozdzielczości, zamiast polegać wyłącznie na console.time, aby można było porównywać i omawiać te liczby.
Następnie zastąp dane wejściowe nieważnymi i uruchom ponownie. Najciekawszym wynikiem w teście 4.5 jest przepustowość przy złym danych wejściowych. Większość osób testuje tylko ważny JSON, ale publiczne punkty końcowe spędzają dużo czasu na odrzucaniu niepoprawnie sformatowanych lub szkodliwych żądań.
Test funkcjonalny poprzedza test szybkości
Zanim porównasz zegary, upewnij się, że każdy parser nadal przekazuje informacje potrzebne twojej interfejsowi użytkownika. Najprostszą metodą sprawdzenia jest przetworzenie błędnego danych i wyświetlenie ścieżki pierwszego problemu:
const a = Invoice.safeParse(bad);
console.log(a.success === false ? a.error.issues[0]?.path : "valid?");
Jeśli skompilowany parser nadal zwraca issues[0].path, formularz, który podświetla błędne pole, może na nie przejść. Jeśli jednak wystąpi błąd bez określenia ścieżki lub zwróci tylko wartość logiczną, nie można już przekazać tej samej informacji o poziomie pola do funkcji useActionState ani żadnego innego mechanizmu zarządzania stanem, który wyświetla błędy formularza.
To jest kluczowy kompromis. Wartość logiczna odpowiada na pytanie „Czy to w ogóle faktura?”, ale nie może podpowiedzieć, które pole jest błędne. Użyj tej wartości logicznej jako bramy wejściowej, na przykład przed procesorem obsługującym wiersze CSV, a funkcję safeParse zachowaj tam, gdzie osoba musi odczytać i naprawić błąd, np. w formularzu, który rysuje czerwoną obwódkę wokół pola na wpisanie adresu e-mail.
Czym zajmuje się kompilacja i czego nie zmienia
z.compile() nie ma nic wspólnego z kompilatorem TypeScript. Koncepcyjnie Zod generuje bardziej specjalistycznego parsera dla danego schematu, dzięki czemu proces przetwarzania wykonywany jest w sposób mniej ogólny i bardziej interpretacyjny. z.infer<typeof Invoice> pozostaje niezmieniony; zmienia się jedynie obiekt używany w czasie wykonywania.
Dwa obiekty, a nie jeden
Kompilowany parser to zrzut stanu schematu w momencie jego skompilowania. Metody schematu Zod, takie jak .optional(), zwracają nowe schematy, więc jeśli później stworzysz wariant, na przykład uczynisz pole email opcjonalnym, i nadal będziesz używać CompiledInvoice, kompilowany parser nadal odrzuci brakujący adres e-mail. To jest prawidłowe zachowanie, a nie błąd – trzymasz wtedy dwa niezależne obiekty. Kompiluj ponownie za każdym razem, gdy zmieni się definicja schematu, i unikaj kodu, który wydaje się modyfikować schemat po jego skompilowaniu.
Kompiluj raz, przy ładowaniu modułu
Kompiluj na poziomie modułu, nigdy wewnątrz obsługi żądania. Kompilowanie na każde żądanie generuje dokładnie tę koszt utworzenia, którą miała amortyzować ta funkcjonalność, i może łatwo spowolnić działanie bardziej niż zwykłe safeParse.
Gdzie oszczędności pamięci są istotne
Zmniejszenie ilości pamięci wynika z sposobu, w jaki instancje schematów przechowują swoje metody. Jeśli twoja aplikacja tworzy schematy tylko raz podczas ładowania modułu, co jest zalecanym podejściem, raczej nie zaobserwujesz znaczącej zmiany w zużyciu pamięci procesu. Oszczędności są istotniejsze, gdy istnieje bardzo duża liczba instancji schematów, więc nie oczekuj, że wartość 784 bajtów pojawi się w twoich wynikach RSS.
Zachowaj to na serwerze
Kompilacja to optymalizacja po stronie serwera. Umieszczenie z.compile w komponencie klienckim, na przykład na stronie ustawień, zwiększa obciążenie i rozmiar pliku do przetworzenia przez przeglądarkę, która będzie musiała przeanalizować co najwyżej kilka danych.
Czytanie wyników
W przypadku tego schematu faktury, po 10 000 iteracjach po rozgrzewce na pojedynczym laptopie, jedna sesja dała następujące wyniki.
Ważne dane wejściowe:
safeParse: 312 ms- skompilowana analiza: 141 ms
Nieważne dane wejściowe:
safeParse: 498 ms- boolean validate: 39 ms
- compiled parse: 188 ms
Żaden z tych wyników nie osiąga poziomu 16 razy szybszego działania, z wyjątkiem operacji boolean validate na nieważnych danych, która jest bliska tego celu. Ulepszenia są zgodne z obietnicami wersji wydanej, choć mniejsze przy bardziej złożonym schemacie. Ten najbardziej imponujący wynik, validate na nieważnych danych, to również funkcja, której nigdy nie należy używać samodzielnie za formularzem.
Kilka innych aspektów zasługuje na osobny wiersz:
- Błędy: w tym teście zaimpaktowana funkcja
safeParsenadal zgłaszała błąd dotyczący nieważnego adresu e-mail, więc formularz mógł nadal go używać. - Kształt API: wynik funkcji
z.compileto odrębna wartość, którą należy traktować jako niezmienialną, stałą funkcję.
Ograniczenia tych wartości
Te wyniki pochodzą z jednego schematu, jednej maszyny i 10 000 iteracji pętli po fazie rozgrzewkowej. Wartość 16x odnosi się do weryfikacji nieprawidłowych danych w własnym teście benchmarku twórców; test ten pokazał duży postęp, a nie wymieniony w nagłówku efekt. Wartość pamięci heap na jeden ciąg znaków nie została sprawdzona oddzielnie. Ponadto ścisła pętla hrtime mierzy koszt obliczeniowy CPU, a nie opóźnienie widoczne dla użytkownika: nic nie mówi o wskaźnikach Web Vitals dla rzeczywistego odwiedzającego w rzeczywistej sieci.
Wasze dane do przesłania będą się różnić. Jeśli wasze schemat zawiera pojedynczy element z.string(), jego skompilowanie nie pomoże w przyspieszeniu wolnego żądania. Jeśli natomiast chodzi o duży, zagnieżdżony obiekt znajdujący się na intensywnie używanej ścieżce, zmierz czas jego przetwarzania. Wykonaj 10 000 prawidłowych i 10 000 nieprawidłowych danych wejściowych oraz zapisz czas przetwarzania wraz z pierwszą ścieżką, która doprowadziła do błędu przy nieprawidłowych danych.
Scenariusz awarii, z którego warto się nauczyć
Rozważmy import pliku CSV, który w pętli wywoływał metodę Invoice.parse. Prawidłowe wiersze były przetwarzane szybko, natomiast nieprawidłowe tworzyły kompleksowe struktury danych, co spowalniało pracę procesora. Zmiana ścieżki walidacji na logiczną wartość booleanową poprawiła wydajność, ale ta sama zmiana została zastosowana również do ścieżki interfejsu użytkownika, która miała podświetlać wartość total. Rezultatem było to, że wartość "na" powodowała ciche odrzucenie bez podania nazwy pola, a błędna wartość trafiała do raportu przez inną ścieżkę, która polegała na wartości booleanowej.
Próba naprawy, która tylko pogorszyła sytuację, polegała na kompilowaniu schematu wewnątrz obsługiżonego elementu przy każdej prośbie; koszt konfiguracji zniweczył wszelkie korzyści.
To, co faktycznie zadziałało:
z.compileprzy ładowaniu modułu, używane przez proces importowania;safeParsew formularzu, gdzie osoba może przeczytać komunikat o błędzie;- ścieżka booleanowa validate służąca jedynie jako filtr przed kolejką zadań.
Ogólne zasady: jeśli skompilowany parser przestanie raportować issues[0].path, formularz nie może go używać.
Zapisz swoje środowisko przed przeprowadzeniem testów wydajnościowych
Wyniki wydajności mają sens tylko w połączeniu z wersjami, które je wygenerowały. W projekcie referencyjnym – małej aplikacji Next.js z czterema trasami związanymi z fakturami – użyto stacku składającego się z Node 24, TypeScript 7 i Next 16.3. Zachowaj notatkę z laboratorium w repozytorium, na przykład w pliku notes/lab.md, aby każda sesja rozpoczynała się od tych samych danych. Najpierw wydrukuj wersje:
node -v
pnpm exec tsc -v
pnpm exec next --version
Zapisz je na górze notatki. Jeśli główna wersja różni się od tej, którą zakłada przewodnik, zatrzymaj się i dopasuj je przed kontynuacją, ponieważ późniejsze polecenia mogą dać wiarygodne, ale mylące wyniki.
Następnie uruchom serwer rozwojowy i przejdź po trasach, aby upewnić się, że aplikacja działa prawidłowo, zanim cokolwiek zmierzasz:
pnpm exec next dev
Odwiedź ponownie /, /invoices, /invoices/1, /settings oraz /invoices, z włączoną opcją „Preserve log” w DevTools, i zapisz stan pola filtrującego wraz z adresem URL.
Następnie uruchom sprawdzacz typów i wydrukuj jego kod zakończenia:
pnpm exec tsc --noEmit --pretty false
echo $?
Kod zakończenia równy zero nie jest dowodem na to, że aplikacja działa poprawnie; oznacza on jedynie, że typy są zgodne, i daje uprawnienie do przystąpienia do sprawdzeń w czasie wykonywania. Po tym samodzielnie uruchom polecenia do pomiarów wydajności z sekcji reprodukcji, zamiast polegać na danych innych osób. Sprzęt, temperatura otoczenia, obciążenie pamięci oraz inne procesy działające na maszynie mogą wpłynąć na czasy wykonywania bardziej niż niewielka aktualizacja biblioteki.
Warto również dodać do każdej notatki jednozdaniową linijkę z opisem „nieudanej próby naprawy” w formie „spróbowałem X, nadal miałem problem Y”. Takie informacje zapewniają uczciwe rejestrowanie postępów i uniemożliwiają ponowne badanie tego samego ślepego zaułka.
Powszechne błędy
- Kompilacja parsera w komponencie klienckim. To powodowało wzrost rozmiaru pliku i skargi narzędzi; rozwiązaniem było przeniesienie go do workera.
- Walidacja logiczna w formularzu. Funkcja zwracała
falsebez podania przyczyny, przez co użytkownik widział bezużyteczny, pusty komunikat błędu; konieczne było przywrócenie funkcjisafeParse. - Zmiana schematu po kompilacji. Kompilowany parser nadal stosował stare zasady; należy ponownie go skompilować.
Listwa kontrolna przed kompilacją schematu
- Kompiluj przy ładowaniu modułu, a nie na żądanie.
safeParse w formularzach i upewnij się, że nadal odczytuje ona issues[0].path.Zachowaj wszystkie pięć wyników pomiaru czasu z własnego testowania. Podanie jedynie najlepszego wyniku dostawcy nic nie mówi o twoim systemie.
Gdzie kompilacja naprawdę się sprawdza
W aplikacji do faktur okazało się, że import CSV jest jedynym miejscem, gdzie kompilacja przynosiła korzyści. Formularz ustawień nigdy jej nie potrzebował, podobnie jak webhook odbierający po jednej fakturze. Mimo to próba kompilacji powodowała inne problemy: gdy w pamięci znajdowały się dwa obiekty schematu, programista założył, że eksportowany schemat to „ten skompilowany”, go zmienił, a działający parser zignorował tę zmianę. Te kompilacje zostały cofnięte.
Zasada, która się sprawdziła, jest krótka:
- kompiluj schematy wykonywane w pętlach lub na naprawdę intensywnie używanych ścieżkach;
- nie kompiluj dla pojedynczego obsługiwanego żądania POST;
- używaj sprawdzenia logicznego jako bramy do kolejki;
- używaj
safeParsetam, gdzie ktoś musi odczytać ścieżkę błędu.
Napisz te zasady jako komentarze na górze pliku schematu – przetrwają one każdy post na blogu czy wątek rozmowy.
// 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
Jeśli nagłówek i kod się różnią, jeden z nich jest błędny, więc należy to poprawić tego samego dnia. Nagłówek z napisem „form: safeParse only” nad formularzem, który faktycznie wywołuje funkcję validate, to dokładnie sposób, w jaki występuje opisany powyżej scenariusz błędu.
Aby zapoznać się z szerszym kontekstem udostępniania schematów pomiędzy klientem a serwerem, zobacz udostępnianie jednego schematu Zod pomiędzy frontendem React a backendem Node.
Główne wnioski
z.compile()oferuje szybszy parser w czasie wykonywania w zamian za jednorazową konfigurację; jest opłacalny tylko wtedy, gdy schemat jest wykorzystywany wiele razy, dlatego należy go kompilować przy ładowaniu modułu na serwerze.- Ścieżka booleanowa validate jest znacznie szybsza przy nieprawidłowych danych, ponieważ pomija drzewo problemów, co jest również powodem, dla którego nie może być używana z formularzami wymagającymi wykrywania błędów na poziomie pól.
Powiązane materiały
- Zastępowanie as-Casts parsowaniem Zod w każdej grenicy danych Next.js — Dlaczego konwersja typów w TypeScript nie chroni przed zmianami API oraz jak jeden schemat Zod waliduje wyniki fetch, formy, obsługę tras i działania serwera w Next.js.
- Ochrona granic Express: Jedno Zod Middleware dla ciała żądania, parametrów i ciągów zapytania — Dowiedz się, jak weryfikować ciała żądań Express, parametry trasy oraz ciągi zapytania za pomocą jednego wielokrotnie używalnego middleware Zod oraz jak uzupełnia on weryfikację modeli Sequelize.
- Identyfikatory z marką w TypeScriptie: Które kodowania naprawdę zapobiegają błędnym operacjom usuwania — Sześć sposobów definiowania typów UserId i InvoiceId porównanych w jednym teście: które z nich powodują odrzucenie operacji deleteInvoice(userId) przez tsc oraz w jakich przypadkach markowanie Zod zapewnia bezpieczeństwo w czasie wykonywania.