Skip to content

Производительность

Тёплый вызов .get() - это один Map.get(key) и прямой new Ctor(...): без reflection, таблиц метаданных и proxy на пути. Цифры в бенчмарке ниже появляются из конкретных решений в рантайме, а не из отдельного fast mode, который нужно включать вручную:

РешениеЭффект
Явные регистрацииСборка контейнера делает плоский Map.set на сервис. Нет побочных эффектов декораторов, парсеров имён конструкторов и таблиц метаданных.
Закешированные singleton- и scoped-сервисыТёплый вызов .get() читает cache.get(key) до проверок циклов и времени жизни. Запасной cache.has(key) нужен только для явно зарегистрированного undefined.
Прямые вызовы конструкторовКлассы с 0-7 зависимостями идут по прямому пути new Ctor(...). Конструкторы с большим числом аргументов используют Reflect.construct.
Асинхронные фабрикиФабричный Promise кешируется как есть, поэтому параллельные вызовы делят одну начатую инициализацию, а .get() остаётся синхронным.
Граница strict modestrict: true ловит циклы и утечки времени жизни. strict: false убирает эти проверки для заранее проверенных горячих transient-графов.

Результаты бенчмарков

Набор бенчмарков

Набор бенчмарков сравнивает InferDI с InversifyJS v8, Awilix v13 в режимах PROXY и CLASSIC, TSyringe v4, TypeDI v0.10 и Typed Inject v5.

Все значения указаны в операциях в секунду на Node 22. Чем выше, тем лучше.

СценарийInferDIInversifyJSTyped InjectAwilix (PROXY)Awilix (CLASSIC)TSyringeTypeDI
1. Горячий singleton (тёплый кеш)14.3 M10.7 M7.0 M7.3 M6.7 M5.8 M6.45 M
2. Transient-сервис (новый экземпляр на вызов)9.75 M6.1 M4.1 M3.45 M3.0 M2.5 M1.6 M
3. Глубокий граф (10 уровней, всё transient)2.3 M1.5 M1.3 M716 k736 k643 k222 k
4a. Широкий граф (4 зависимости, корень transient)8.25 M4.9 M3.4 M2.2 M2.3 M1.65 M1.1 M
4b. Широкий граф (10 зависимостей, корень transient)3.5 M1.9 M2.6 M1.2 M1.3 M938 k458 k
5. Сборка контейнера и первый вызов400 k13.2 k223 k10 k8.3 k206 k282 k
6. Жизненный цикл scope (создание, resolve, очистка)2.85 M35 k2.45 M330 k430 k1.1 M665 k
7. Ленивый resolve (обёртка с отложенным доступом)11.8 M7.6 M7.15 M5.6 M4.7 M4.25 M2.85 M

Что показывают цифры

  • Закешированный singleton-resolve в 1,34 раза быстрее ближайшей альтернативы — InversifyJS.
  • Сборка контейнера вместе с первым вызовом выигрывает за счёт плоской регистрации. InferDI регистрирует граф с нуля; библиотеки на декораторах уже оплатили часть этой работы при загрузке модулей.
  • Сценарии с широким графом показывают пользу развёрнутых вызовов по числу аргументов. При четырёх зависимостях InferDI опережает ближайшую альтернативу в 1,68 раза. На десяти зависимостях он переходит на Reflect.construct и остаётся в 1,35 раза быстрее Typed Inject.
  • Жизненный цикл scope включает создание scope, resolve и очистку. Сценарий 6 выполняет dispose на каждой итерации, поэтому измеряет владение scope, а не только получение значения.
  • InferDI лидирует во всех 8 сценариях. Typed Inject остаётся ближайшей альтернативой в scoped-потоках и широком графе с десятью зависимостями, а InversifyJS — при закешированном singleton, transient, глубоком графе и широком графе с четырьмя зависимостями.

Быстрый режим

new Container({ strict: false }) убирает runtime-учёт циклов, отслеживание singleton-стека и try/finally вокруг защищённого пути resolve. В README пакета показано ускорение примерно на 30% для локальных transient-вызовов на плоском transient-графе. Закешированные singleton- и scoped-вызовы не меняются, потому что возвращаются до этих проверок.

Включайте быстрый режим только после тестов, которые прогнали граф в стандартном strict mode. TypeScript не видит singleton-циклы, transient-циклы, динамические ключи, as-касты и фабрики, захватившие внешний контейнер с более широким типом.

Для профилированного production-пути с большим числом transient-resolve { strict: false } даёт максимальную поддерживаемую скорость после такой проверки. В разработке и тестах оставляйте strict mode. На закешированные singleton- и scoped-resolve этот режим не влияет.

Детали горячего пути

Symbol-ключи могут помочь в плотных циклах resolve, потому что Map сравнивает их по идентичности. Строковым ключам нужен хеш, а при коллизии - посимвольное сравнение. В большинстве приложений разница не измеряется, поэтому переходите на symbol-ключи только после сигнала профилировщика.

Запуск локально

bash
cd benchmarks
pnpm install --frozen-lockfile
pnpm run precondition
pnpm run bench

Рабочее пространство бенчмарков изолировано от корневого workspace pnpm и имеет собственный lockfile. Методология и фикстуры описаны в benchmarks/README.md.