У клієнтських вебзастосунках (Next.js Client Components, SPA, мікрофронтенди) розмір JavaScript-бандла безпосередньо визначає ключові метрики продуктивності Core Web Vitals: Total Blocking Time (TBT) та Interaction to Next Paint (INP). Що більший обсяг завантажуваного скрипту, то довше процесор мобільного пристрою виконує парсинг і компіляцію в рушії V8, блокуючи основний потік браузера.
Історичний лідер екосистеми — бібліотека Zod — спроєктована за об'єктно-орієнтованою моделлю з ланцюжковим синтаксисом (Fluent Interface):
z.string().min(5).max(100).email().trim();При такій архітектурі всі можливі методи валідації та трансформації жорстко прив'язані до прототипу класу ZodString та базового ZodType. Алгоритми усунення мертвого коду (Tree-Shaking) у сучасних бандлерах (Webpack, Rollup, Turbopack, Vite) аналізують граф статичних імпортів на рівні ES-модулів. Оскільки методи класу зв'язані єдиним монолітним об'єктом, бандлер не здатний визначити, які методи використовуються в застосунку, а які ні. У результаті імпорт навіть найпростішої схеми z.string() призводить до включення всієї бібліотеки Zod повністю (близько 55–70 КБ нестисненого коду / ~14 КБ gzip) у клієнтський бандл.
Valibot вирішує цю проблему радикальним архітектурним переходом до чистої функціональної декомпозиції. У Valibot повністю відсутні монолітні класи. Кожен примітив (string, number), кожен валідатор (email, minLength, regex) та кожна трансформація (trim, toLowerCase) являють собою незалежні експортовані функції вищого порядку.
Сполучною ланкою виступає конвеєр pipe(), що об'єднує базову схему типу з ланцюжком валідаційних та трансформаційних дій (Actions). Бандлер включає у фінальний JavaScript-бандл виключно ті функції, які були явно імпортовані розробником, дозволяючи знизити розмір коду валідації форми до менше ніж 1 КБ (gzip) (скорочення ваги до 90–95%).
Різниця в графі залежностей між ООП-ланцюжками та модульними функціями наочно пояснює механізм роботи Tree-Shaking:
+-------------------------------------------------------------------------+
| МОНОЛІТНИЙ ГРАФ ZOD (Tree-Shaking неможливий) |
+-------------------------------------------------------------------------+
| import { z } from 'zod'; |
| z.string().email(); |
| |
| Бандлер бачить клас ZodString: |
| class ZodString { |
| min() { ... } // <- Тягнеться в бандл, навіть якщо не потрібен! |
| max() { ... } // <- Тягнеться в бандл! |
| uuid() { ... } // <- Тягнеться в бандл! |
| regex() { ... } // <- Тягнеться в бандл! |
| ip() { ... } // <- Тягнеться в бандл! |
| } |
| => Підсумкова вага бандла: ~14 КБ (gzip) |
+-------------------------------------------------------------------------+
| |
| МОДУЛЬНИЙ ГРАФ VALIBOT (Повний Tree-Shaking) |
+-------------------------------------------------------------------------+
| import { object, string, email, pipe } from 'valibot'; |
| |
| Бандлер аналізує ES-експорти: |
| - object() => Включений у бандл (~350 байтів) |
| - string() => Включений у бандл (~150 байтів) |
| - email() => Включений у бандл (~200 байтів) |
| - pipe() => Включений у бандл (~100 байтів) |
| * Усі інші 150+ функцій бібліотеки повністю видалені зі збірки! |
| => Підсумкова вага бандла: ~850 байтів (gzip)! |
+-------------------------------------------------------------------------+Перший аргумент pipe() перевіряє базовий тип (string, number, object). Якщо тип не збігається, виконання негайно зупиняється.
Дані передаються лінійно крізь функції трансформації (trim, toLowerCase) та валідатори (minLength, email).
При виявленні невідповідностей дія генерує об'єкт Issue зі строгою вказівкою шляху (path) та повідомлення.
Метод safeParse повертає типізований об'єкт { success: true, output } або { success: false, issues }.
Розглянемо практичну реалізацію валідації профілю користувача:
import * as v from 'valibot';
// Опис схеми через функціональний конвеєр pipe()
export const UserProfileSchema = v.object({
id: v.pipe(
v.string(),
v.uuid('Ідентифікатор має бути валідним UUID v4')
),
username: v.pipe(
v.string(),
v.trim(), // Трансформація: автоматичне видалення пробілів
v.minLength(3, 'Мінімальна довжина імені — 3 символи'),
v.maxLength(20, 'Максимальна довжина імені — 20 символів')
),
email: v.pipe(
v.string(),
v.trim(),
v.toLowerCase(), // Трансформація: нормалізація регістру
v.email('Некоректний формат email-адреси')
),
age: v.pipe(
v.number(),
v.minValue(18, 'Сервіс доступний лише особам старше 18 років'),
v.maxValue(100, 'Вкажіть коректний вік')
),
website: v.optional(
v.pipe(
v.string(),
v.url('Некоректний формат URL-адреси')
)
),
});
// Статичний інференс типу вхідних даних (до трансформацій)
export type UserProfileInput = v.InferInput<typeof UserProfileSchema>;
// Статичний інференс типу вихідних даних (після trim та toLowerCase)
export type UserProfileOutput = v.InferOutput<typeof UserProfileSchema>;[!NOTE] Зверніть увагу на розділення
InferInputтаInferOutput. Оскільки конвеєр містить трансформації (trim(),toLowerCase()), вхідні дані можуть відрізнятися від вихідних за своєю формою або типом, що суворо відстежується системою типів TypeScript.
Valibot дозволяє створювати власні валідатори без роздування кодової бази за допомогою функцій check або низькорівневих інтерфейсів rawCheck:
import * as v from 'valibot';
// Кастомна дія перевірки надійності пароля
export function passwordStrength(requirementMessage = 'Пароль повинен містити щонайменше 1 цифру та 1 спецсимвол') {
return v.check((input: string) => {
const hasNumber = /\d/.test(input);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(input);
return hasNumber && hasSpecialChar;
}, requirementMessage);
}
// Кастомна дія валідації податкового номера (10 цифр із контрольною сумою)
export function isValidTaxId(errorMessage = 'Некоректний формат податкового коду організації') {
return v.check((input: string) => {
if (!/^\d{10}$/.test(input)) return false;
const coefficients = [2, 4, 10, 3, 5, 9, 4, 6, 8];
const controlSum = coefficients.reduce(
(sum, coeff, idx) => sum + coeff * Number(input[idx]),
0
);
const controlDigit = (controlSum % 11) % 10;
return Number(input[9]) === controlDigit;
}, errorMessage);
}
// Використання кастомних дій у конвеєрі
export const SecurityFormSchema = v.object({
taxId: v.pipe(
v.string(),
v.trim(),
isValidTaxId()
),
password: v.pipe(
v.string(),
v.minLength(8, 'Довжина пароля — не менше 8 символів'),
passwordStrength()
),
});У клієнтських формах слід завжди використовувати safeParse для запобігання генерації необроблених винятків (Uncaught Exceptions):
import * as v from 'valibot';
import { UserProfileSchema, UserProfileOutput } from './profile-schema';
export interface FormSubmissionResult {
isSuccess: boolean;
data?: UserProfileOutput;
fieldErrors?: Record<string, string>;
}
export function handleProfileSubmission(rawFormData: unknown): FormSubmissionResult {
// safeParse не викидає винятків і повертає типізований результат
const result = v.safeParse(UserProfileSchema, rawFormData);
if (!result.success) {
// Перетворення плаского масиву issues на мапу помилок для полів форми
const fieldErrors: Record<string, string> = {};
for (const issue of result.issues) {
// Вилучення імені першого сегмента шляху помилки (наприклад, 'email' або 'username')
const fieldName = issue.path?.[0]?.key;
if (typeof fieldName === 'string' && !fieldErrors[fieldName]) {
fieldErrors[fieldName] = issue.message;
}
}
return {
isSuccess: false,
fieldErrors,
};
}
// Дані гарантовано валідні та трансформовані
return {
isSuccess: true,
data: result.output,
};
}Створимо готовий архітектурний модуль із доменними валідаторами для клієнтських форм автентифікації:
import * as v from 'valibot';
// 1. Базові перевикористовувані будівельні блоки
export const EmailSchema = v.pipe(
v.string(),
v.trim(),
v.toLowerCase(),
v.email('Некоректний синтаксис корпоративного email'),
v.maxLength(100, 'Email не повинен перевищувати 100 символів')
);
export const PasswordSchema = v.pipe(
v.string(),
v.minLength(8, 'Пароль повинен містити щонайменше 8 символів'),
v.maxLength(64, 'Пароль не повинен перевищувати 64 символи')
);
// 2. Схема входу
export const LoginSchema = v.object({
email: EmailSchema,
password: PasswordSchema,
rememberMe: v.optional(v.boolean(), false),
});
// 3. Схема реєстрації з валідацією збігу паролів через forward/check
export const RegistrationSchema = v.pipe(
v.object({
email: EmailSchema,
password: PasswordSchema,
confirmPassword: v.string(),
agreeToTerms: v.pipe(
v.boolean(),
v.check((val) => val === true, 'Необхідно підтвердити згоду з умовами оферти')
),
}),
// Перевірка збігу двох паролів на рівні всього об'єкта
v.forward(
v.check(
(input) => input.password === input.confirmPassword,
'Введені паролі не збігаються'
),
['confirmPassword'] // Помилка прив'язується до поля confirmPassword
)
);
export type LoginCredentials = v.InferOutput<typeof LoginSchema>;
export type RegistrationPayload = v.InferOutput<typeof RegistrationSchema>;Розробники, які переходять із Zod, за звичкою пишуть v.string().min(5) або v.object().required(). Це призводить до помилки TypeError: v.string(...).min is not a function, оскільки v.string() повертає простий дескриптор схеми без методів. Будь-які валідації мають загортатися у функцію v.pipe().
// ❌ ЗЛАМАНО: спроба виклику методу класу
const BrokenSchema = v.string().minLength(5); // TypeError!
// ✅ ПРАВИЛЬНО: функціональна композиція через pipe
const CorrectSchema = v.pipe(v.string(), v.minLength(5));Функція pipe() виконує дії строго зліва направо. Якщо розмістити валідатор email() до дії нормалізації trim(), рядок " [email protected] " буде відхилений валідатором через початкові пробіли ще до того, як trim() встигне їх видалити. Трансформації даних повинні передувати валідаторам формату.
// ❌ ПОГАНО: email() впаде через пробіли до виклику trim()
const BadOrder = v.pipe(v.string(), v.email(), v.trim());
// ✅ ПРАВИЛЬНО: спочатку очищення даних, потім валідація
const GoodOrder = v.pipe(v.string(), v.trim(), v.email());Метод v.parse() при будь-якій помилці валідації викидає виняток ValiError. Використання його в обробниках форм без блоку try/catch призводить до краху всього компонента та показу білого екрана помилки замість виведення тексту під інпутами. У клієнтському коді форм завжди використовуйте v.safeParse().
Імпорт import * as v from 'valibot' не шкодить Tree-Shaking, якщо бандлер налаштований правильно, проте імпорт непотрібних допоміжних утиліт у серверні файли може випадково затягнути зайві поліфіли. Намагайтеся використовувати іменований імпорт конкретних функцій: import { object, string, pipe } from 'valibot'.
Виконується перевірка кореневого типу (string, number, boolean). При невідповідності пайплайн негайно зупиняється.
Дані передаються в першу дію (наприклад, нормалізація trim()), яка повертає модифіковане значення.
Очищене значення проходить крізь серію предикатів (minLength, email). Помилки фіксуються в масиві issues.
Якщо масив issues порожній, safeParse повертає статус успіху з трансформованим значенням output.