Производительность
Тёплый вызов .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 mode | strict: true ловит циклы и утечки времени жизни. strict: false убирает эти проверки для заранее проверенных горячих transient-графов. |

Набор бенчмарков
Набор бенчмарков сравнивает InferDI с InversifyJS v8, Awilix v13 в режимах PROXY и CLASSIC, TSyringe v4, TypeDI v0.10 и Typed Inject v5.
Все значения указаны в операциях в секунду на Node 22. Чем выше, тем лучше.
| Сценарий | InferDI | Typed Inject | Awilix (PROXY) | Awilix (CLASSIC) | InversifyJS | TSyringe | TypeDI |
|---|---|---|---|---|---|---|---|
| 1. Горячий singleton (тёплый кеш) | 14.2 M | 7.0 M | 7.2 M | 6.9 M | 6.3 M | 6.2 M | 6.4 M |
| 2. Transient-сервис (новый экземпляр на вызов) | 8.4 M | 4.3 M | 3.4 M | 2.9 M | 3.4 M | 2.4 M | 1.6 M |
| 3. Глубокий граф (10 уровней, всё transient) | 1.85 M | 1.28 M | 701 k | 739 k | 750 k | 601 k | 214 k |
| 4a. Широкий граф (4 зависимости, корень transient) | 7.3 M | 3.2 M | 2.2 M | 2.3 M | 2.3 M | 1.6 M | 1.1 M |
| 4b. Широкий граф (10 зависимостей, корень transient) | 3.5 M | 2.6 M | 1.2 M | 1.3 M | 1.6 M | 1.0 M | 437 k |
| 5. Сборка контейнера и первый вызов | 400 k | 228 k | 10 k | 8 k | 13 k | 202 k | 272 k |
| 6. Жизненный цикл scope (создание, resolve, очистка) | 2.66 M | 2.39 M | 492 k | 413 k | 28 k | 1.08 M | 637 k |
| 7. Ленивый resolve (обёртка с отложенным доступом) | 12.1 M | 7.0 M | 5.5 M | 4.7 M | 4.2 M | 4.0 M | 2.8 M |
Что показывают цифры
- Закешированный singleton-resolve примерно в 2 раза быстрее ближайшей альтернативы в этом наборе.
- Сборка контейнера вместе с первым вызовом выигрывает за счёт плоской регистрации. InferDI регистрирует граф с нуля; библиотеки на декораторах уже оплатили часть этой работы при загрузке модулей.
- Сценарии с широким графом показывают пользу развёрнутых вызовов по числу аргументов. До семи зависимостей V8 может заинлайнить прямой вызов конструктора. На десяти зависимостях InferDI переходит на
Reflect.constructи всё равно лидирует среди сравниваемых библиотек. - Жизненный цикл scope включает создание scope, resolve и очистку. Сценарий 6 выполняет dispose на каждой итерации, поэтому измеряет владение scope, а не только получение значения.
- Typed Inject остаётся ближайшей альтернативой. Его граф, известный на этапе компиляции, хорошо держится на глубоких графах и scoped-потоках.
Быстрый режим
new Container({ strict: false }) убирает runtime-учёт циклов, отслеживание singleton-стека и try/finally вокруг защищённого пути resolve. В README пакета показано ускорение примерно на 30% для локальных transient-вызовов на плоском transient-графе. Закешированные singleton- и scoped-вызовы не меняются, потому что возвращаются до этих проверок.
Включайте быстрый режим только после тестов, которые прогнали граф в стандартном strict mode. TypeScript не видит singleton-циклы, transient-циклы, динамические ключи, as-касты и фабрики, захватившие внешний контейнер с более широким типом.
Детали горячего пути
Symbol-ключи могут помочь в плотных циклах resolve, потому что Map сравнивает их по идентичности. Строковым ключам нужен хеш, а при коллизии - посимвольное сравнение. В большинстве приложений разница не измеряется, поэтому переходите на symbol-ключи только после сигнала профилировщика.
Запуск локально
cd benchmarks
pnpm install --frozen-lockfile
pnpm run precondition
pnpm run benchРабочее пространство бенчмарков изолировано от корневого workspace pnpm и имеет собственный lockfile. Методология и фикстуры описаны в benchmarks/README.md.
