Галоўная / Артыкулы / Як баг типу „Getter“ таямніча пасла Зоду у 3 разы гorsшую працездатнасць

Як баг типу „Getter“ таямніча пасла Зоду у 3 разы гorsшую працездатнасць

Уважліва аналіза таго, як механізм выдачы гетэраў CommonJS у TypeScript завадзіў функцыям інлайнінгу V8 у Zod, а таксама таго, што змянілася пад час апштурхованага перапісву Zod 4.

1719 слоў

Проблема: гетэры невидны для JIT

Калі TypeScript кампайляе запаведзенне перэкспорту, такое як export * from './schemas', ён не проста копіюе значэнні. У замене ён стварае гетэр для кожнага экспортаванага імені — маленькую функцыю, якая выконваецца кожны раз, калі чытаецца атрыбут, замест таго каб практычна статычны атрыбут, які безпосередзя храніць значэнне.

Зазвычай гэта ўскладненне рэалізацыі, якое ніхто не зазначае. У случаю Zod гэта мела вельмі важлівыя наследкі: 252 з 255 экспортных функцый у точцы входу CommonJS версіі Zod 4.5 былі рэалізаваны як гетеры. JIT-кампайляр V8 адлейнаеся да викорыстоўвання інлайн-коду — замены вызову функцыі на яе самае тэло, ў результате чаго двыжок ухіляецца ад додатковых витрачэнняў, — але толькі калі можа гарантаваць, што цяльная функцыя застойна і прыемлівала. Гетер нарушае гэтую гаранцыю. V8 не можа бачыць фіксаваную, незменную функцыю, якая ховаецца за гетерам, таму не можа безпечна інлайняваць нічога, што практычна доступна такім спосабам.

Рашэнне ў версіі Zod 4.6 здаецца практычна занадта простым: выдаваць звычайныя атрыбуты заместо гетэраў, а таксама заморажваць ствараны об’ект экспортных функцый, каб V8 знаў, што ён ніколі не будзе змянены. Ось як гэты падход выглядае на практыцы:

// CommonJS require — this is the path that was affected
const { z } = require("zod");
const CompiledPlayer = z.compile(Player);
z.validate(CompiledPlayer, data);
// ~3x faster in Zod 4.6 than the identical call in Zod 4.

Важна точна інформацыя пра тое, насколькі вузка ў рэальнасці гэтае рашэнне, адколькі лёгка пераценіць його дыяметр. Паўпрабны ўплыв мелі толькі вызовы, якія направляюцца через об’ект namespace — такія як z.validate(...) або z.compile(...) — і толькі пад час викорыстоўвання require(). Вызов методу безпасна на экземпляре схэмы, напрыклад Player.safeParse(data), ніколі не паўтараеся ў об’екте exports, таму такі падход ніколі не быў паўпрабным. Конструкцыя ESM засталася абсолюютна недастраўненай; гэта была строга особлівасць механізма перэекспорту CommonJS.

Значэнне гэтага факту выходзіць далека за межы самога Zod: формат выходных дадзенняяў кампайлера мае рэальныя наследкі пад час выканання, якія не маюць нічынага з логікай, яку вы насправдзе напісалі. Ніхто, які выкарыстоўваў Zod 4.5, не пісаў гэршага коду, чым тыя, хто выкарыстоўваў 4.6 — аднаковы вызыв z.validate() проста выканавляўся быстрей, таму што адна з дапаможных устаткуванняў, кампайлер TypeScript, проста структурувала своі выходныя дадзеныя інакш.

Большая перапісва, якая стоіць за гэтым

Эта парадыгма ўскладнення — толькі малая частка значна большага процесу: Zod 4, стабільны з 2025 года, ўзначальваецца з нуля, і яго прырост скорасці ўласна є значным. Незалежныя тэсты паказалі, што парсаванне простых стрынгоў вядзецца працоўна ў адносе 14 разоў быстрэй, масівы — працоўна ў адносе 7 разоў быстрэй, а парсаванне об’ектаў — працоўна ў адносе 6,5 разоў быстрэй, усё гэта па вылічэннях на адной засадзе Zod 3. Але змяна, якая больш за ўсё можа паўлінуць на вашы ўласны робочы процес, зовсім не стосуецца скорасці выканання: Колькасць інстанцыяў компілятора TypeScript для типовага схематызму зменілася з адной часткі 25 000 да працоўна 175 — гэта і ўсвятляе, чаму раней редактары та апараты перакранчвання типоў спамілелі працаваць у вялікіх кодбазах, наполненых Zod, і чаму часта вони больш не спамілеюць.

Для ситуацыяў, дзе размер пакета мае важнае значэнне — функцыі на краю сеті, віджэты з боку кліента — Zod Mini апранае той самы набор верыфікатораў через абліковую, можнаю да скасавання частей інтэрфейс, а не за звычайным стылем Zod з ланцоўкамі методаў:

// Standard Zod — method chaining
import * as z from "zod";
const User = z.object({ name: z.string(), age: z.number() });
// Zod Mini - same validators, functional style, smaller bundle
import * as z from "zod/mini";
const User = z.object({ name: z.string(), age: z.number() });

Што на самай працы змянілася ў API

Этае тое месца, дзе просты каманд npm install zod@^4 можа тыха зламаць існуючы код, таму варта рассмотрзець кожную змяну безпосередна, а не пакладацца на статысцю змян.

Верыфікаторы формата страк сталі функцыямі вышэйшага рангу, якія можна скасаваць частей:

// Zod 3 style — deprecated, but still works
const schema = z.string().email();
// Zod 4 - the new standard
const schema = z.email();
const id = z.uuid();
const site = z.url();

Чатыры адзелёныя механізмы для налаштавання паведамленняяў пра адказы былі з’еднаны ў адну опцыю:

// ❌ Zod 3 — three different mechanisms
const schema = z.string({
  required_error: "Name is required",
  invalid_type_error: "Name must be a string",
});
const age = z.number({
  errorMap: (issue, ctx) => {
    if (issue.code === "too_small") return { message: "Must be 18+" };
    return { message: ctx.defaultError };
  },
});
// ✅ Zod 4 - one parameter, string or function
const schema = z.string({ error: "Name is required" });
const age = z.number({
  error: (issue) => {
    if (issue.code === "too_small") return "Must be 18+";
    return "Invalid age";
  },
});

Форматаванне адказаў пра адказы было выведзена з об’екта адказу і ператворана на самастоятельныя памагальныя функцыі:

const result = User.safeParse(input);
if (!result.success) {
  result.error.issues;              // the raw array - was .errors in Zod 3
  z.treeifyError(result.error);     // nested shape, replaces .format()
  z.flattenError(result.error);     // { formErrors, fieldErrors }, replaces .flatten()
  z.prettifyError(result.error);    // human-readable string, great for logs
}

Тыповы канал API, напісанный з викорыстаннем Zod 4, выглядае так:

app.post("/users", (req, res) => {
  const result = User.safeParse(req.body);
  if (!result.success) {
    const { fieldErrors } = z.flattenError(result.error);
    return res.status(400).json({ errors: fieldErrors });
  }
  // result.data is fully typed here
  createUser(result.data);
});

Тонкія пасткі, якія варта выявіць окрема

Два зі змян, якія былі введены ў Zod 4, належаць да адной конкрэтнай категорыяй небяспекі: яны прахадзяюць через перагляд коду без выклікання жадных паведамленняў, а потым з’являюцца як багі ў працоўным режыме за калькі недзеяў. Іх усіх варта выявіць окрема, а не захаваць у списку.

ZodError.errors больш не існуе, яго заменіў .issues. Якшо якая-небудзь з вашых існуючых логік адработкі бягаў прымае ў вартоść error.errors, ніч не будзе выклікана. Ёй проста тыха будзе выдана вартоść undefined. Такія проблемы прахадзяюць через будзь-які набор тэстаў, які не пераканаецца спецыяльна ў гэтый конкрэтны атрыбут, і стаюць виднымі толькі тады, калі рэальны корыстнік ступіць на яе ў працоўным режыме.

Прыорытэт паведамленняў пра адказкі ў контексте быў зменены. У версіі Zod 3 паведамленне пра адказку, заданае пад час парсавання, мела прыорытэт перад тым, якое было заданае ў самай схеме. У версіі Zod 4 гэты прыорытэт зменіўся: тепер перамагае паведамленне на рэвэлі схемы.

const mySchema = z.string({ error: () => "Schema-level error" });
// Zod 3: this override wins → "Contextual error"
// Zod 4: the schema-level error wins instead → "Schema-level error"
mySchema.parse(12, { error: () => "Contextual error" });

Нічога не змянілася ў месцы вызову, пры тым жа той самы код вяртае разныя паведамленні, залежна ад таго, якая версія запускаецца — гэта змена ў працоўнасці, якая маскуюцца пад простыя змены назв.

Чыстая картына конкурэнціі

Хоча і є спадчына чытаць апдэйт 4.6 і шырэйшую перапісву як доказ таго, што Zod тепер без працы перамагае ўсі іншыя бібліятэкі верыфікацыі, рэальныя цыфры вымагаюць болей адзёванай заключэння. Выконваючы мільйон пераканальных проверкаў над вялікім об’ектам з восьмаю палаткай на апараты M3 Pro, ArkType завершае ўсё за адносна 820 мс, Valibot — за адносна 1 140 мс, а Zod 4 — за адносна 1 380 мс. Для параджу, Zod 3 для выкаання тых сабе задач трэбаваў адносна 4 200 мс, таму перапісва ёст калічытным і значным прыростам у пораўнанні з яго сабе попераднім версіяй, нават якщо ён і не стае лепшым у всіх аспектах. Чыстка па розмеру пакетаў: Valibot залишаецца з большым прыростам — типовая схема формы для заходу мае розмер адносна 1,37 КБ з Valibot, у працоўніку стандартны Zod — адносна 17,7 КБ, а нават з Zod Mini — яшчэ блізка да 7 КБ.

Болейш адпрацоўны вывод з тых сабе цифр — гэта тое, што пры праўільнасці ад мільйона пераверк за секунду — што значна больш, чым патрэбна для функцыонавання будзь-якага рэалістычнага канцэнтра API — разліка ў выконвальных характерыстыках між гэтымі трыма бібліятекамі выражаецца ў калькольках сотак мілісэкунд за мільйон вызоў. Такая разліка ніколі не будзе помечана звычайным трафікам у працэсе вырабніцтва. Для сервісаў Node.js і кодавых баз, пабудованых пераважна на tRPC, глыбэйшая падтрымка экасістэмы Zod і яго знакомы стыль аўтаматызаваных методаў зазвычай маюць большое значэнне ў павсякдзенным выкарыстоўванні, чым тое, якая бібліятека выграе ў синтэтычных тэстах. У ситуацыях, дзе розмер пакета ёсць справжнім обмежэнням — напрыклад, у крайніх функцыях або пераверкачах, якія надаюцца кліенту — прыродны перавага Valibot у розмере ёсць тым фактарам, які насправдзе вялі за выбор, незалежна ад таго, насколькі быстра кожная з гэтых бібліятек выкарыстоўваецца для пераверкі.

Практычныя наставы па міграцыі

Перш за ўсё, пераканайцеся, што у вас TypeScript 5.5 або новейшая версія, адколькі для Zod 4 гэта неабходна. Методы, якія былі занедбаны і перашлі з Zod 3, продовжаюць працаваць, але выдаюць поперадзевыя паведамлення, і самэ гэта ўсё частая прычына таго, чаму большасць команд переходзіць на новую версію поступова, файл за файлам, замест таго, каб спробаваць ризыкаваны моментальны перехід. Найважлівейшы крок, які трэба здзеўсці першым, — это пошук у всім кодбэсе элементаў .errors, .format() і .flatten(), якія викорыстоўваюцца з об’ектамі памилак Zod, адколькі самэ гэтыя змены выражаюцься без явных сігналоў проблемы. А якщо ваш проект вже на Zod 4, але працюе через механізм CommonJS require() у Node — што ўсё яшчэ часта трапляецца у бэкенд-расстановках, нават у кодбэсах, якія іншым чынам викорыстоўваюць ESM — то апдэйт да версіі 4.6 практычна ўзначыць безкоштовная павышэння працэсу, адколькі для гэтага не трэба робіць жадных змян у вашам коде.

Асалідны вывадак

Історыя парадксу з 4.6 ёсць простая, але урок, які з яё вытыкаецца, ўжо большы: тое, што насправды працюе ў продакшэне, вярнейша, заўсёды залежыць толькі наполовіну ад коду, які вы пішалі. Іншая половіна вялічыні залежыць ад таго, што ваш кампайлер і бандлер выберуць выдаты ў ваша імя, і гэты выданы слой мае свою сопрацоўнасць, якая не мае нічынага з тым, насколькі адзяйна была написана ваша сопрацоўнасць. Большую частку часу можна без апасцярыбы ігнораваць гэты слой цалкам. Але час ад часу — як у случае з 252 методамі-гетарамі, якія тыхо блакавалі функцію-інлайнер V8 прыгожа ад годзіны — варта памяцать, што «мой код правільны» і «мой код компілюецца ў штось быстрае» — гэта два разныя тверджэння. Другое з іх варта перыядычна пераканацца, нават калі нічога з таго, што вы зрабілі, насправды не было неправильна.

Спадневаная літэратура

  • Фіксы Node.js 26, які таямніча заменяюць гадзіны раштаркавання — аптака пра API Temporal у Node.js 26, выконанне TypeScript на натыўнай адзыначэнні, дапаможныя прыборы для кэшаў і іншыя дадаткі, якія усунулі старэ ўсвайомленыя раштаркавання.