Zod 4.5 — компіляція, перевірка та safeParse: вибір парсера за межею
Як протестувати компільовані парсери Zod 4.5 та швидкий шлях перевірки булевих значень на коректних та некоректних даних, а також визначити, до якої API належить кожен випадок, не втрачаючи інформації про шляхи виникнення помилок.
Джерела: Zod 4.5 z.compile() Cut My Parse Time. Invalid Payloads Got Faster Than Valid Ones.
Якщо обробник маршруту починається з schema.safeParse(body), ви платите за обробку даних за допомогою Zod при кожному запиті. У версії Zod 4.5, оголошеній 28 серпня 2026 року, додано функцію z.compile(), щоб частина цієї роботи виконувалася лише один раз, а також швидкий шлях перевірки типу на рівні булевих значень, який уникає створення деталей помилок. Швидше парсинг корисний лише тоді, коли інформація про помилки, від якої залежить інтерфейс користувача, залишається цілою; тому цей посібник показує, як виміряти час виконання цих трьох варіантів для реалістичної схеми з коректним та пошкодженим вхідним даними, а також як вирішити, який API підходить для кожної частини додатку.
Що насправді обіцяє версія 4.5
Корисно відокремлювати офіційні заяви розробників від заголовків, які поширюються пізніше. Згідно з оголошенням про версію 4.5:
- будь-яку схему можна попередньо скомпілювати за допомогою
z.compile(schema), що прискорює процес парсингу;
z.validate() — це булевий швидкий механізм, який, за описом, працює вдванадцять разів швидше з недійсними даними, оскільки не створює повного дерева проблем;z.string() зберігала приблизно 7,5 kB у версії 4.4 та лише 784 байти у версії 4.5.Вважайте ці значення показниками від розробників. Фраза „до 16 разів“ стосується найкращого варіанту, тож сприймайте її як напрямок покращень, а не як конкретний результат, якого слід очікувати. На момент написання цього тексту цей API ще новий, тому перевіряйте назви та сигнатури функцій у поточній документації Zod та у вашій встановленій версії, перш ніж покладатися на будь-яку інформацію нижче.
Реалістична схема для тестування
Мікротестування з використанням однієї функції z.string() мало що розповідають про додаток. Більш достовірний тест використовує схему, схожу на реальний трафік: рахунок-фактуру з ідентифікатором, електронною поштою, загальною сумою, валютою, списком позицій та необов’язковим записом метаданих. Нічого не спрощується лише для того, щоб компілятор виглядав краще.
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>;
Зверніть увагу на три моменти. Об’єкт CompiledInvoice створюється лише один раз, у межах модуля. Визначений тип все ще походить від початкової схеми Invoice, адже компіляція змінює поведінку під час виконання, а не типи. Крім того, обидва об’єкти експортуються, що, як ви побачите пізніше, має певні наслідки.
Для вхідних даних використовуйте два пакети даних, які відображають те, що насправді отримує ваша система. Валідний пакет містить три елементи, валюту BDT та відомий ключ метаданих. Невалідний пакет поєднує типові помилки користувачів: total: „na“ (клітка таблиці, скопійована у поле чисел), порожній масив lines та email: „not-an-email“.
Вимірювання часу обробки трьох парсерів
Інструмент проводить 1 000 ітерацій кожним шляхом, щоб JIT налаштувався, а потім вимірює 10 000 ітерацій, зчитуючи значення process.hrtime.bigint() до та після кожного циклу, усе це в межах одного процесу.
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);
Два застереження перед тим, як копіювати цей код. По-перше, перевірте, яку форму написання використовує ваша версія для булевого шляху; у фрагменті коду цей елемент викликається як метод у схемі, тоді як у описі згадується z.validate(). Якщо під час встановлення доступний лише safeParse, це означає, що у вас немає версії 4.5, і результати тестування для версії 4.4 не повинні вважатися результатами компіляції. По-друге, виклик parse при недійсному вхідному даних спричиняє помилку, тож під час вимірювання часу обробки пошкоджених даних потрібно або обгорнути цей цикл у try/catch, або використати версію парсера без виникнення помилок; інакше цикл зупиниться при першому пошкодженому даних, і отримане число буде беззмістовним.
Відтворення вимірювань
Встановіть останню версію та підтвердьте, яку саме версію ви отримали:
pnpm add zod@latest
pnpm exec node -e "import * as z from 'zod'; console.log('zod', z.VERSION ?? 'read package.json')"
Розмістіть схему у файлі src/invoice.ts, а цикли – у файлі src/bench.ts. У Node 24 файл TypeScript, який використовує лише синтаксис, що може бути видалений, може запускатися безпосередньо завдяки вбудованій функції прибирання типів:
node src/bench.ts
Якщо ваш код використовує синтаксис, який не можна просто видалити, запускайте його замість цього за допомогою команди pnpm exec tsx src/bench.ts. Який би варіант ви не обрали, використовуйте один і той самий інструмент для всіх трьох циклів та виводьте значення часу з високою роздільною здатністю, а не покладайтеся лише на console.time, щоб можна було порівнювати та обговорювати ці числа.
Потім підставте недійсний даний та запустіть програму знову. Найцікавішим результатом у тесті 4.5 є пропускна здатність при недійсних даних вхіду. Більшість людей тестують лише коректний JSON, але публічні кінцеві точки витрачають багато часу на відхилення некоректних або шкідливих запитів.
Функціональний тест передує тесту швидкості
Перш ніж порівнювати годинники, переконайтеся, що кожен парсер все ще повідомляє те, що потрібно вашому інтерфейсу. Найпростіший спосіб перевірки — це обробити некоректний пакет даних та вивести шлях першої проблеми:
const a = Invoice.safeParse(bad);
console.log(a.success === false ? a.error.issues[0]?.path : "valid?");
Якщо скомпільований парсер все ще повертає issues[0].path, форма, яка підсвічує проблемне поле, може переключитися на нього. Якщо він кидає помилку без вказання шляху або повертає лише булеве значення, ви більше не зможете передавати те саме повідомлення на рівні поля до функції useActionState чи будь-якого іншого механізму керування станом, який відображає помилки форми.
Це і є основний компроміс. Булеве значення відповідає на запитання „Чи це взагалі рахунок-фактура?“ але не може вказати, яке саме поле є неправильним. Використовуйте булеве значення як фільтр, наприклад перед процесором, який обробляє рядки CSV, та залишайте safeParse там, де людина повинна прочитати та виправити помилку, наприклад у формі, яка намальовує червону рамку навколо поля введення електронної пошти.
Що змінює та що не змінює компіляція
z.compile() не має нічого спільного з компілятором TypeScript. Концептуально Zod генерує більш спеціалізований парсер для конкретної схеми, тож основний шлях виконання виконує менше універсальної, інтерпретаційної роботи. z.infer<typeof Invoice> залишається незмінним; змінюється лише об’єкт часу виконання, який ви викликаєте.
Два об’єкти, а не один
Компільований парсер є копією схеми у момент її компіляції. Методи схеми Zod, такі як .optional(), повертають нові схеми, тому якщо пізніше ви створите варіант, наприклад зробивши email необов’язковим, але продовжите використовувати CompiledInvoice, компільований парсер все одно відхилить запис без електронної пошти. Це правильна поведінка, а не баг: ви працюєте з двома незалежними об’єктами. Перекомпілюйте код щоразу, коли змінюється визначення схеми, та уникайте коду, який ніби змінює схему після її компіляції.
Компілюйте один раз під час завантаження модуля
Компілюйте на рівні модуля, ніколи всередині обробника запитів. Компіляція за кожним запитом спричиняє саме ту вартість підготовки, яку призначено було розподілити, і це може легко уповільнити роботу більше, ніж звичайний safeParse.
Де важлива економія пам’яті
Зменшення об’єму пам’яті виникає через те, як екземпляри схем зберігають свої методи. Якщо ваше додатко створює свої схеми один раз під час завантаження модуля, що є рекомендованим підходом, ймовірно, ви не побачите значних змін у використанні пам’яті процесу. Економія стає значущою, коли існує дуже велика кількість екземплярів схем, тому не очікуйте, що значення 784 байт з’явиться у вашому RSS.
Зберігайте на сервері
Компіляція є оптимізацією з боку сервера. Розміщення z.compile у клієнтському компоненті, наприклад на сторінці налаштувань, додає роботу та збільшує об’єм пакету для браузера, який буде обробляти щонайбільше кілька навантажень.
Читання результатів
За цією схемою рахунку-фактури, після 10 000 ітерацій після розігріву на одному ноутбуці, один запуск дав такі результати.
Дійсні навантаження:
safeParse: 312 мс- компільована обробка: 141 мс
Недійсні дані навантаження:
safeParse: 498 мс- boolean validate: 39 мс
- compiled parse: 188 мс
Жоден з цих показників не досягає значення 16 разів, хоча показник для boolean-функції з недійсним вхідним даним наближається до цього значення. Покращення відповідають тому, що обіцялося у новій версії, просто їх масштаб менший для складніших схем. Єдиний вражаючий показник — validate з недійсними даними — це той, якого ні в якому разі не слід використовувати окремо після форми.
Ще кілька аспектів заслуговують окремого рядка кожен:
- Помилки: у цьому тесті скомпільований
safeParseвсе ще виявляв проблему з некоректною електронною поштою, тож форма могла продовжувати її використовувати. - Форма API: результат
z.compile— це окреме значення, яке слід розглядати як незмінну, фіксовану функцію.
Обмеження цих чисел
Ці результати отримано за допомогою однієї схеми, одного комп’ютера та після 10 000 ітерацій циклу після розігріву. Число 16x стосується перевірки некоректних даних у власному тестуванні розробників; цей тест показав значне покращення, але не саме цей показник є ключовим. Показник кількості пам’яті на одну стрічку не аналізувався окремо. Крім того, жорсткий цикл hrtime вимірює витрати CPU, а не затримку для користувача: він нічого не говорить про показники Web Vitals для справжнього відвідувача у реальній мережі.
Ваші навантаження будуть різними. Якщо ваша схема — це один елемент типу z.string(), його компіляція не допоможе прискорити повільний запит. Якщо ж це великий вкладений об’єкт на завантаженій доріжці, виміряйте час його обробки. Виконайте 10 000 коректних та 10 000 некоректних вхідних даних та зафіксуйте час виконання, а також шлях першої проблеми під час обробки некоректних даних.
Сценарій збою, який варто вивчити
Розгляньмо імпорт у форматі CSV, який у циклі викликав функцію Invoice.parse. Коректні рядки оброблялися швидко, але некоректні створювали повні структури проблем, через що процес обробки сповільнювався. Зміна механізму перевірки на булеву функцію виправила проблему з продуктивністю, але ця сама зміна була застосована також до інтерфейсу, який мав підкреслювати значення total. У результаті значення "na" призводило до тихої відмови без зазначення назви поля, а пошкоджене значення потрапляло до звіту через іншу доріжку, яка покладалася на булеве значення.
Спроба виправлення, яка лише погіршила ситуацію, полягала у компіляції схеми всередині обробника з кожним запитом; витрати на налаштування знищили всі переваги.
Що насправді спрацювало:
z.compileпід час завантаження модуля, який використовується робочим процесом імпорту;safeParseу формі, де людина може прочитати повідомлення про помилку;- булевий параметр validate використовується лише як фільтр перед чергою обробки.
Існує правило: якщо скомпільований парсер припиняє повідомляти про issues[0].path, форма не може ним користуватися.
Запишіть своє середовище перед тим, як проводити тестування
Результати продуктивності мають сенс лише у поєднанні з версіями, які їх створили. У прикладному проекті — невеликому додатку Next.js із чотирма маршрутами, пов’язаними з рахунками-фактурами — використовувалася стек Node 24, TypeScript 7 та Next 16.3. Зберігайте записи про експерименти у репозиторії, наприклад у файлі notes/lab.md, щоб кожна сесія починалася з однакових даних. Спочатку виведіть версії:
node -v
pnpm exec tsc -v
pnpm exec next --version
Запишіть їх у верхній частині запису. Якщо основна версія відрізняється від тієї, яку припускає посібник, зупиніться та узгодьте її перед продовженням, адже подальші команди можуть дати правдоподібні, але оманливі результати.
Далі запустіть сервер розробки та пройдіться по маршрутам, щоб переконатися, що додаток працює нормально, перш ніж щось вимірювати:
pnpm exec next dev
Відвідайте знову /, /invoices, /invoices/1, /settings та /invoices, увімкнувши опцію „Зберегти журнал“ у DevTools, та зафіксуйте стан поля фільтра разом із URL-адресою.
Потім запустіть перевірку типів та виведіть її статус завершення:
pnpm exec tsc --noEmit --pretty false
echo $?
Код завершення 0 не є доказом того, що додаток працює; це лише означає, що типи узгоджуються, і дає можливість переходити до перевірок під час виконання. Після цього самостійно запустіть команди тестування продуктивності з розділу відтворення, а не покладайтеся на чужі дані. Апаратне забезпечення, температура навколишнього середовища, навантаження на пам’ять та інші фактори, які працюють на комп’ютері, можуть суттєво вплинути на час виконання, навіть більше, ніж невелике оновлення бібліотеки.
Також варто додавати до кожної записки рядок із описом „невдалої спроби виправлення“ у форматі „спробував X, але все одно отримав Y“. Такі рядки забезпечують точність записів та запобігають подвійному дослідженню одних і тих самих нерозв’язаних проблем.
Поширені помилки
- Компільований парсер у клієнтському компоненті. Розмір бандлу збільшувався, а інструменти скаржились; вирішенням було переміщення його до воркера.
- Перевірка типу даних у формі. Вона повертала
falseбез пояснень, через що користувач залишався без корисної інформації щодо помилки; довелося повернути використанняsafeParse. - Зміна схеми після компіляції. Компільований парсер зберігав старі правила; краще перекомпілювати схему.
Чек-лист перед компіляцією схеми
- Компілюйте при завантаженні модуля, а не за кожним запитом.
safeParse у формах та переконуйтесь, що вони все ще читають issues[0].path.Зберігайте всі п’ять значень часу з вашого власного тестування. Наведення лише найкращих результатів від постачальника нічого не говорить про вашу систему.
Де компіляція справді має сенс
У додатку для рахунків-фактур імпортер CSV виявився єдиним місцем, де компіляція була корисною. Форма налаштувань ніколи її не потребувала, як і webhook, який отримує по одному рахунку-фактурі. Проте її використання спричиняло іншу проблему: коли в пам’яті знаходилися два об’єкти схеми, розробник припустив, що експортована схема — це „скомпільована“, змінив її, але запущений парсер проігнорував ці зміни. Ці компіляції були скасовані.
Політика, яка допомогла, є короткою:
- компілюйте схеми, які виконуються всередині циклів або на справді важливих шляхах;
- не компілюйте їх для окремого обробника POST;
- використовуйте булеву перевірку як бар’єр у черзі;
- використовуйте
safeParseтам, де людина мусить читати інформацію про помилку.
Запишіть ці правила у вигляді коментарів на початку файлу схеми — там вони залишаться незмінними довше, ніж будь-яка стаття у блогу чи тема чату:
// 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
Якщо заголовок та код не збігаються, хтось із них є неправильним, тож потрібно виправити його того ж дня. Заголовок, який містить текст "form: safeParse only" над формою, яка насправді викликає функцію validate, саме так і призводить до описаної вище ситуації збою.
Щоб дізнатися більше про спільне використання схем між клієнтом та сервером, перегляньте спільне використання однієї схеми Zod між вашим React-фронтендом та Node-бекендом.
Ключові висновки
z.compile()замінює одноразову налаштування на швидший парсер під час виконання; це ефективно лише тоді, коли схема використовується багато разів, тому її слід компілювати під час завантаження модуля на сервері.- Булевий шлях validate працює значно швидше з некоректним вхідним даним, оскільки пропускає структуру виявлення помилок, і саме тому його ніколи не слід використовувати для форм, які потребують виявлення помилок на рівні полів.