Benchmark лучших практик

Joi реализует декларативную модель описания схем, где каждая структура данных проходит последовательную цепочку проверок. Основная стоимость валидации формируется не в одном месте, а распределяется между несколькими этапами:

  • разбор схемы и построение внутреннего представления;
  • обход структуры входного объекта;
  • применение правил преобразования;
  • выполнение кастомных и встроенных валидаторов;
  • формирование результата и ошибок.

Ключевая особенность заключается в том, что каждая операция добавляет вычислительную нагрузку, которая становится заметной при массовой валидации (например, API под высокой нагрузкой или обработка потоков событий).


Компиляция схем и повторное использование

Одна из наиболее значимых точек оптимизации — повторное использование заранее созданных схем.

Joi позволяет создавать схемы один раз и применять их многократно:

const schema = Joi.object({
  id: Joi.number().integer().required(),
  name: Joi.string().min(3).max(30),
  active: Joi.boolean()
});

Антипаттерн: динамическое создание схем

Частая ошибка — генерация схемы внутри обработчика:

function validate(data) {
  const schema = Joi.object({
    id: Joi.number().required(),
    name: Joi.string().required()
  });

  return schema.validate(data);
}

Такой подход приводит к:

  • повторной аллокации объектов схемы;
  • повторной сборке правил;
  • увеличению GC-нагрузки.

Оптимальный подход

Схема создаётся один раз и кэшируется:

const userSchema = Joi.object({
  id: Joi.number().required(),
  name: Joi.string().required()
});

function validateUser(data) {
  return userSchema.validate(data);
}

Синхронная и асинхронная валидация

Joi поддерживает оба режима:

  • синхронный (validate)
  • асинхронный (validateAsync)

Асинхронный режим становится необходимым при использовании:

  • кастомных асинхронных валидаторов;
  • внешних проверок (например, запросов к базе данных);
  • сложных цепочек external().

Бенчмаркинговое наблюдение

Синхронная валидация значительно быстрее за счёт отсутствия Promise-обвязки. Асинхронная версия добавляет:

  • создание Promise;
  • планирование микротасков;
  • обработку await-цепочек.

При массовой обработке простых структур предпочтение отдаётся синхронному режиму.


Управление поведением валидации

abortEarly

Параметр контролирует, останавливается ли проверка при первой ошибке.

schema.validate(data, { abortEarly: false });

Поведение:

  • true — минимальная стоимость выполнения;
  • false — полная проверка всех полей.

Бенчмаркинговый эффект

При abortEarly: false нагрузка растёт пропорционально количеству ошибок и глубине объекта. Это критично для больших схем с десятками полей.


convert

schema.validate(data, { convert: true });

Преобразование типов (например, строка → число) требует дополнительной логики:

  • парсинг;
  • проверка допустимости;
  • создание нового значения.

Отключение конвертации уменьшает стоимость, но переносит ответственность на входные данные.


stripUnknown

schema.validate(data, { stripUnknown: true });

Удаление неизвестных полей включает:

  • обход всех ключей объекта;
  • фильтрацию по схеме.

При больших объектах операция становится затратной, особенно при глубокой вложенности.


Глубина объектов и стоимость обхода

Валидация Joi работает рекурсивно. Каждый уровень вложенности увеличивает:

  • число вызовов валидаторов;
  • количество проверок типов;
  • нагрузку на стек вызовов.

Пример проблемной структуры

const schema = Joi.object({
  user: Joi.object({
    profile: Joi.object({
      settings: Joi.object({
        theme: Joi.string()
      })
    })
  })
});

Глубокая вложенность приводит к экспоненциальному росту операций при массивных коллекциях объектов.


Валидация массивов

Массивы — одна из наиболее затратных структур.

Joi.array().items(
  Joi.object({
    id: Joi.number(),
    value: Joi.string()
  })
);

Основные источники нагрузки:

  • итерация по каждому элементу;
  • применение схемы к каждому элементу;
  • проверка правил items, ordered, unique.

Оптимизационный подход

  • избегание избыточных unique;
  • минимизация вложенных объектов внутри массивов;
  • предварительная фильтрация данных до Joi.

Кастомные валидаторы и их стоимость

Кастомные правила:

Joi.string().custom((value, helpers) => {
  if (!value.startsWith('X')) {
    return helpers.error('any.invalid');
  }
  return value;
});

Бенчмаркинговые особенности:

  • каждый кастомный вызов — функция в JS-рантайме;
  • отсутствие оптимизаций движка Joi;
  • невозможность батчинга операций.

При массовой обработке кастомные валидаторы становятся узким местом.


Кэширование и фабрики схем

Проблема динамической генерации

Иногда схемы зависят от параметров:

function createSchema(max) {
  return Joi.object({
    value: Joi.number().max(max)
  });
}

Если функция вызывается часто — возникает избыточная нагрузка.

Решение через кэширование

const schemaCache = new Map();

function getSchema(max) {
  if (!schemaCache.has(max)) {
    schemaCache.set(max, Joi.object({
      value: Joi.number().max(max)
    }));
  }
  return schemaCache.get(max);
}

Поведение ошибок и стоимость формирования отчёта

Формирование ошибок включает:

  • накопление результатов;
  • генерацию структурированного описания;
  • нормализацию путей (path).

При abortEarly: false стоимость возрастает за счёт хранения массива ошибок.


Бенчмаркинговые методики

Для измерения производительности используются стандартные подходы:

  • повторные прогоны (warm-up);
  • усреднение результатов;
  • исключение первого запуска (JIT-компиляция);
  • контроль GC-пауз.

Пример базового теста:

console.time('joi');

for (let i = 0; i < 100000; i++) {
  schema.validate({ id: 1, name: 'test' });
}

console.timeEnd('joi');

Более точный подход

Использование специализированных библиотек:

  • Benchmark.js;
  • встроенные профилировщики Node.js;
  • flamegraph-анализ.

Сравнение паттернов использования

Паттерн 1: минимальная схема

  • низкая глубина;
  • отсутствие кастомных валидаторов;
  • отключённый abortEarly: false.

→ максимальная производительность.


Паттерн 2: богатая доменная схема

  • вложенные объекты;
  • кастомные правила;
  • преобразования типов.

→ средняя и высокая нагрузка CPU.


Паттерн 3: динамическая генерация схем

  • создание схем в runtime;
  • отсутствие кэширования.

→ наихудший вариант по производительности.


Практики снижения нагрузки

Упрощение схем

Снижение количества вложенных правил уменьшает число операций обхода.


Разделение схем

Большие схемы делятся на независимые части:

  • базовая валидация;
  • расширенная бизнес-валидация.

Предварительная нормализация данных

Чистка данных до Joi снижает:

  • количество преобразований;
  • количество ошибок;
  • нагрузку на кастомные правила.

Контроль опций валидации

Баланс параметров:

  • abortEarly: true для высоконагруженных систем;
  • stripUnknown: false при строгих API-контрактах;
  • convert: false при контролируемом входе.

Поведение в высоконагруженных системах

При обработке тысяч запросов в секунду ключевым фактором становится:

  • стабильность GC;
  • отсутствие динамических аллокаций схем;
  • минимизация глубины объектов;
  • снижение количества кастомных функций.

Joi при неправильной конфигурации становится источником непредсказуемых пиков задержек, особенно при росте вложенности данных и увеличении количества правил на поле.