Inline-компиляция

В основе работы Ajv лежит преобразование JSON Schema в высокопроизводительные JavaScript-функции. Вместо интерпретации схемы во время каждой проверки данных библиотека один раз компилирует её в код, который затем выполняется напрямую. Такой подход и называется inline-компиляцией: логика валидации «встраивается» в сгенерированную функцию без промежуточных абстракций.

Принцип работы компилятора схем

Каждая схема в Ajv проходит этап компиляции, на котором JSON-структура превращается в JavaScript-код. Этот код описывает последовательность проверок значений, условий типов, ограничений и ссылок на другие схемы.

Вместо обхода дерева схемы при каждом вызове проверки формируется специализированная функция:

  • проверка типов становится typeof-условиями;
  • ограничения превращаются в прямые сравнения;
  • логические операторы схемы (allOf, anyOf, oneOf) становятся ветвлениями;
  • ссылки $ref разрешаются в вызовы других скомпилированных функций.

Результатом является функция, эквивалентная ручной реализации валидации.

Inline-компиляция как генерация исполняемого кода

Inline-компиляция в Ajv означает, что логика схемы не остаётся в виде структуры данных, а внедряется непосредственно в тело функции.

При компиляции создаётся строка кода, которая затем превращается в функцию через механизм Jav * aScript:

  • генерация строки с кодом проверки;
  • объединение частей схемы в единый блок;
  • создание функции через new Function(...).

Такой подход позволяет избежать накладных расходов на интерпретацию схемы в рантайме.

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

Пример компиляции схемы

import Ajv from "ajv";

const ajv = new Ajv();

const schema = {
  type: "object",
  properties: {
    age: { type: "number", minimum: 18 }
  },
  required: ["age"]
};

const validate = ajv.compile(schema);

validate({ age: 25 }); // true
validate({ age: 15 }); // false

В момент вызова ajv.compile(schema) происходит генерация функции, которая примерно эквивалентна:

function validate(data) {
  if (typeof data !== "object" || data === null) return false;
  if (!("age" in data)) return false;
  if (typeof data.age !== "number") return false;
  if (data.age < 18) return false;
  return true;
}

Структура генерируемого кода

Генератор кода Ajv формирует валидатор по блокам:

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

Каждый блок вставляется напрямую в тело функции, что и формирует inline-стиль исполнения.

Кэширование скомпилированных схем

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

Механизмы кэширования включают:

  • привязку к $id;
  • внутренний реестр схем;
  • повторное использование $ref.
ajv.addSchema(schema, "userSchema");

const v1 = ajv.getSchema("userSchema");
const v2 = ajv.getSchema("userSchema");

Обе функции используют один и тот же скомпилированный валидатор.

Inline-компиляция и ссылки $ref

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

const addressSchema = {
  type: "object",
  properties: {
    city: { type: "string" }
  }
};

const userSchema = {
  type: "object",
  properties: {
    address: { $ref: "addressSchema" }
  }
};

После компиляции addressSchema становится отдельной функцией, а userSchema вызывает её внутри своего тела.

Это предотвращает разрастание кода и сохраняет производительность.

Генерация через Function constructor

Ajv использует динамическую генерацию функций:

const validate = new Function("data", generatedCode);

где generatedCode — строка, содержащая весь алгоритм проверки.

Такой подход обеспечивает:

  • максимальную скорость выполнения;
  • отсутствие интерпретатора схемы;
  • возможность агрессивной оптимизации кода движком V8.

Производительность inline-компиляции

Inline-компиляция даёт значительный прирост производительности по сравнению с интерпретацией JSON Schema.

Причины:

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

На практике это означает, что после первой компиляции валидация становится близкой по скорости к ручному JavaScript-коду.

Различие между интерпретацией и компиляцией

Без компиляции:

  • схема читается как объект;
  • каждый вызов проходит по дереву;
  • проверки выполняются динамически.

С inline-компиляцией:

  • схема превращается в функцию;
  • дерево исчезает;
  • выполняется готовый алгоритм.

Таким образом, Ajv смещает нагрузку на этап инициализации.

Standalone-компиляция

Inline-компиляция может использоваться на этапе сборки проекта, формируя полностью автономные валидаторы.

import Ajv from "ajv";
import standaloneCode from "ajv/dist/standalone";

const ajv = new Ajv();
const schema = { type: "string" };

const validate = ajv.compile(schema);
const moduleCode = standaloneCode(ajv, validate);

Результатом становится файл, содержащий готовую функцию без зависимости от Ajv.

Такой подход используется для:

  • уменьшения bundle size;
  • ускорения загрузки;
  • исключения runtime-компиляции.

Inline-компиляция в сборщиках

При использовании bundler’ов (Webpack, Rollup, Vite) скомпилированные схемы могут быть встроены в конечный бандл как обычные функции.

Это позволяет:

  • выполнять валидацию без загрузки Ajv в рантайме;
  • ускорять старт приложения;
  • уменьшать количество зависимостей.

Ограничения механизма inline-компиляции

Использование динамической генерации кода накладывает ограничения:

  • среда с запрещённым Function constructor (CSP) блокирует выполнение;
  • отладка сгенерированного кода сложнее;
  • возможны ограничения в sandbox-окружениях.

В таких случаях требуется альтернативная стратегия или предкомпиляция.

Пользовательские ключевые слова и генерация кода

Ajv позволяет расширять систему через пользовательские keyword’ы, которые также участвуют в inline-компиляции.

ajv.addKeyword({
  keyword: "even",
  type: "number",
  code(cxt) {
    const { data } = cxt;
    cxt.fail(`(${data} % 2 !== 0)`);
  }
});

Такие расширения встраиваются прямо в сгенерированную функцию, сохраняя модель inline-компиляции.

Повторное использование скомпилированных валидаторов

Скомпилированная функция может использоваться многократно без повторной генерации:

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

Компиляция вложенных и сложных схем

При работе со сложными структурами:

  • массивы схем компилируются в циклы;
  • вложенные объекты становятся вложенными блоками условий;
  • комбинированные схемы (allOf, anyOf) превращаются в цепочки проверок.

Это приводит к формированию специализированного кода для каждой структуры данных.

Оптимизационные особенности генератора

Inline-компиляция включает ряд оптимизаций:

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

Эти оптимизации выполняются на этапе генерации кода, а не во время выполнения.