Главная / Статьи

Zod 4.5: компиляция, валидация и safeParse: выбор парсера в зависимости от границы

Как провести тестирование производительности скомпилированных парсеров Zod 4.5 и быстрого пути проверки булевых значений для валидных и невалидных данных, а также определить, к какому API относится каждый элемент, не теряя информацию о путях возникновения ошибок.

Издатель: REACTAPP.TOP11 мин чтения2605 слов

Источники: 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() — это быстрый булевой метод, который, согласно описанию, работает в 16 раз быстрее при обработке невалидных данных, поскольку не создаёт полное дерево ошибок;
  • объём памяти, занимаемой каждой схемой, резко снизился: простой метод z.string() занимал около 7,5 кБ в версии 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 раз, за исключением показателя для логики проверки на недействительные данные, который близок к этому значению. Улучшения соответствуют обещаниям версии продукта, хотя их масштабы меньше при более сложной структуре данных. Единственный выдающийся показатель — проверка недействительных данных — также является тем, который ни в коем случае нельзя использовать отдельно за формой.

    Ещё несколько аспектов заслуживают отдельного упоминания:

    • Ошибки: в ходе этого теста функция safeParse после компиляции всё равно выдавала информацию о проблемном адресе электронной почты, поэтому форма могла продолжать её использовать.
    • Структура API: результат функции z.compile представляет собой отдельное значение, которое следует рассматривать как неизменную, фиксированную функцию.
  • Пакет: скомпилированный парсер не должен находиться в клиентской зоне, например в той, что используется для фильтрации таблиц.
  • Когнитивная нагрузка: теперь существует три API для парсинга, поэтому необходимо определить политику для каждой границы и записать её рядом со схемой.
  • Ограничения этих показателей

    Эти результаты получены на основе одной схемы, одного компьютера и 10 000 итераций цикла после разогрева. Цифра 16x относится к проверке некорректных данных в собственном тесте разработчиков; этот тест показал значительное улучшение, но не именно такой результат был опубликован в заголовках новостей. Показатель объёма памяти на одну строку не анализировался независимо. Кроме того, строгий цикл hrtime измеряет затраты процессора, а не задержку для пользователя: он ничего не говорит о показателях 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 $?
    

    Код завершения равный нулю не является доказательством корректной работы приложения; он лишь означает, что типы совпадают, и позволяет перейти к проверкам во время выполнения. После этого сами запустите команды для тестирования производительности из раздела воспроизведения, не полагаясь на чужие данные. Аппаратное обеспечение, температура окружающей среды, нагрузка на память и другие факторы могут сильно повлиять на время выполнения, даже больше, чем обновление небольшой библиотеки.

    Также стоит добавлять в каждую запись одно предложение с описанием «неудачной попытки решения» в формате «пробовал X, но всё равно возникло Y». Такие записи обеспечивают точность отчетности и предотвращают повторную попытку преодоления той же проблемы.

    Распространенные ошибки

    • Компиляция парсера в компоненте клиента. Это приводило к увеличению размера пакета и жалобам инструментов; решением было переместить парсер в worker.
    • Проверка значений типа boolean на форме. Она возвращала false без указания причины, из-за чего пользователь оставался с бесполезным пустым сообщением об ошибке; пришлось восстановить функцию safeParse.
    • Изменение схемы после компиляции. Компилированный парсер сохранял старые правила; вместо этого следует перекомпилировать код.

    Чек-лист перед компиляцией схемы

    • Компилировать при загрузке модуля, а не по каждому запросу.
  • Используйте скомпилированные парсеры только в рабочих процессах и обработчиках маршрутов, никогда не в клиентских компонентах.
  • Используйте функцию safeParse для форм и убедитесь, что она по-прежнему считывает значение issues[0].path.
  • Используйте логическое значение validate path только там, где достаточно ответа «да» или «нет».
  • Протестируйте 10 000 валидных и 10 000 невалидных данных согласно вашей собственной схеме.
  • Сохраняйте все пять значений времени из собственных тестов. Указание только лучших показателей поставщика ничего не говорит о вашей системе.

    Где компиляция действительно приносит пользу

    В приложении для счетов-фактур импортер 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 работает значительно быстрее с некорректными данными, поскольку пропускает структуру обработки ошибок, и именно по этой причине она никогда не должна использоваться для форм, требующих отображения ошибок на уровне полей.
  • Всегда тестируйте недействительные и действительные пакеты данных, а также проверяйте пути возникновения ошибок перед сравнением скорости.
  • Компилированный парсер представляет собой отдельную копию; перекомпилируйте его при изменении схемы.
  • Рассматривайте коэффициенты, указанные поставщиком, как ориентир, фиксируйте версии компонентов и записывайте правила парсинга рядом с схемой.