Ленивая загрузка схем

Суть ленивой загрузки валидационных схем

Валидация JSON в Ajv основана на заранее скомпилированных схемах. Однако в реальных приложениях полный набор схем часто слишком велик для загрузки на старте. Ленивый подход заключается в том, что схема подгружается и компилируется только в момент, когда она действительно требуется для валидации конкретного объекта.

Основная идея:

  • минимизировать стартовую нагрузку
  • загружать схемы по требованию
  • кэшировать уже скомпилированные схемы
  • поддерживать разрешение $ref на внешние ресурсы

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


Механизм $ref как точка входа для ленивой загрузки

Ленивая загрузка в Ajv почти всегда начинается с использования ссылок $ref.

{
  "$id": "user.json",
  "type": "object",
  "properties": {
    "profile": { "$ref": "profile.json" }
  }
}

При встрече $ref валидатор должен:

  1. определить URI схемы
  2. проверить наличие в кеше
  3. при отсутствии — загрузить схему
  4. скомпилировать её
  5. продолжить валидацию

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


Конфигурация Ajv для ленивой загрузки

Для включения ленивого разрешения зависимостей используется опция loadSchema.

import Ajv from "ajv";

const ajv = new Ajv({
  strict: true,
  loadSchema: async (uri) => {
    const response = await fetch(uri);
    if (!response.ok) {
      throw new Error(`Не удалось загрузить схему: ${uri}`);
    }
    return await response.json();
  }
});

Роль loadSchema

Функция:

  • вызывается при обнаружении неизвестного $ref
  • должна вернуть JSON-схему
  • может использовать HTTP, файловую систему, CDN или динамический import

Асинхронная компиляция схем

Ленивая загрузка невозможна без асинхронной компиляции.

Ajv предоставляет метод:

const validate = await ajv.compileAsync(schema);

Поведение:

  • если схема содержит внешние $ref, они разрешаются через loadSchema
  • компиляция становится асинхронной цепочкой
  • результат кешируется внутри экземпляра Ajv

Кэширование как основа производительности

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

Ajv автоматически:

  • сохраняет схему по $id
  • кэширует скомпилированные функции валидаторов
  • повторно использует уже загруженные $ref

Дополнительно можно управлять поведением через addSchema:

ajv.addSchema(schema, "user.json");

Это позволяет заранее прогреть кеш и уменьшить количество ленивых загрузок.


Разрешение цепочек зависимостей

Сложные схемы часто образуют граф зависимостей:

user.json → profile.json → address.json → country.json

При ленивой загрузке Ajv выполняет:

  1. загрузку user.json
  2. обнаружение $refprofile.json
  3. загрузку profile.json
  4. рекурсивное разрешение вложенных $ref

Важно учитывать:

  • порядок загрузки не детерминирован
  • возможны параллельные запросы к одной и той же схеме
  • требуется защита от повторной загрузки (deduplication)

Контроль параллельных загрузок

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

Типичный паттерн оптимизации:

const cache = new Map();

function loadSchema(uri) {
  if (cache.has(uri)) {
    return cache.get(uri);
  }

  const promise = fetch(uri).then(r => r.json());
  cache.set(uri, promise);

  return promise;
}

Такой слой часто добавляется поверх Ajv loadSchema.


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

В Node.js и современных сборщиках возможна ленивость на уровне модулей:

const ajv = new Ajv({
  loadSchema: async (uri) => {
    if (uri === "profile.json") {
      const mod = await import("./schemas/profile.json", {
        assert: { type: "json" }
      });
      return mod.default;
    }
  }
});

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


Ошибки и особенности асинхронной валидации

При использовании ленивой загрузки:

  • любая валидация становится потенциально асинхронной
  • нельзя использовать синхронный validate(), если есть внешние $ref

Типичная ошибка:

  • вызов валидатора до завершения compileAsync

Правильный подход:

const validate = await ajv.compileAsync(schema);

const valid = validate(data);

Ограничения ленивой загрузки

1. Непредсказуемая латентность

Каждый $ref может вызвать сетевой запрос, что увеличивает время первой валидации.

2. Ошибки сети

Если loadSchema использует HTTP:

  • сбой сети прерывает компиляцию
  • необходимо реализовать retry-логику

3. Сложность отладки

Глубокие цепочки $ref усложняют трассировку ошибок схем.


Практическая стратегия оптимизации

В реальных проектах ленивую загрузку комбинируют с частичной предзагрузкой:

1. Базовые схемы загружаются заранее

await ajv.addSchema(baseSchemas);

2. Редко используемые схемы — лениво

Через loadSchema.

3. Критические схемы кэшируются локально

Включаются в бандл приложения.


Интеграция с микросервисной архитектурой

В распределённых системах схемы часто хранятся централизованно:

  • schema registry
  • API gateway
  • versioned endpoints

loadSchema становится клиентом реестра:

loadSchema: async (uri) => {
  return fetch(`https://schemas.internal/${uri}`).then(r => r.json());
}

Поддержка версионирования:

  • user@1.2.0.json
  • user@latest.json

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

Ленивая загрузка даёт выигрыш в:

  • времени старта приложения
  • объёме памяти
  • размере бандла

Но увеличивает:

  • время первой валидации
  • сложность инфраструктуры

Оптимальный баланс достигается через:

  • агрессивное кеширование
  • предзагрузку горячих схем
  • ограничение глубины $ref

Поведение Ajv при повторной валидации

После первой компиляции:

  • все зависимости уже разрешены
  • $ref заменяются на оптимизированные функции
  • дальнейшая валидация выполняется синхронно

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


Типичные архитектурные ошибки

1. Отсутствие кеша в loadSchema

Приводит к повторным запросам одной и той же схемы.

2. Использование синхронной валидации при внешних $ref

Приводит к runtime-ошибкам.

3. Циклические зависимости схем

A → B → C → A

Без корректного кеширования это приводит к бесконечным запросам.


Роль $id и нормализация URI

Ajv использует $id как ключ регистрации схемы.

Важно:

  • все схемы должны иметь стабильный $id
  • URI должен быть нормализован
  • относительные пути разрешаются относительно базового URI

Комбинация с code splitting

В frontend-приложениях ленивую загрузку часто совмещают с:

  • динамическим import()
  • разделением схем по роутам
  • lazy-loaded feature modules

Каждый модуль приносит свой набор JSON Schema, которые регистрируются в Ajv только при активации функциональности.