Производительность
Записанный результат
В публичном прогоне от 2026-08-17 проверяемый режим по умолчанию разрешал тёплый singleton с медианой 6.233 ns/op. Только в этом сценарии и для записанных версий пакетов медианы других участников были в 1.76–13.05 раза выше. Эти числа измеряют накладные расходы контейнера, а не число HTTP-запросов в секунду или производительность всего приложения. Исходный результат содержит все восемь раундов и данные окружения.
Вызов .get() сначала читает Map.get(key). При попадании в кэш он сразу возвращает значение. Если сервис нужно создать, контейнер вызывает конструктор напрямую. Бенчмарки охватывают следующие особенности реализации:
| Решение | Эффект |
|---|---|
| Явные регистрации | Сборка контейнера делает плоский Map.set на сервис. Нет побочных эффектов декораторов, парсеров имён конструкторов и таблиц метаданных. |
| Закэшированные singleton- и scoped-сервисы | Тёплый вызов .get() читает cache.get(key) до проверок циклов и времени жизни. Явный undefined хранится как внутренний UNDEFINED_MARKER, поэтому при попадании в кэш по-прежнему достаточно одного поиска. |
| Прямые вызовы конструкторов | Классы с 0-7 зависимостями идут по прямому пути new Ctor(...). Конструкторы с большим числом аргументов используют Reflect.construct. |
| Асинхронные фабрики | Фабричный Promise кэшируется как есть, поэтому параллельные вызовы делят одну начатую инициализацию, а .get() остаётся синхронным. |
| Контракт времени выполнения | Режим по умолчанию (fast: false) сохраняет проверки и поиск по точной изменяемой цепочке родителей. fast: true отключает проверки и упрощает поиск в фиксированном дереве скоупов. |
Набор бенчмарков
Сравнительный набор измеряет InferDI (fast) и InferDI (default) вместе с InversifyJS v8, Awilix v13 в режимах PROXY и CLASSIC, TSyringe v4, TypeDI v0.10 и Typed Inject v5. В исходном результате записаны версии пакетов и параметры машины, на которой выполнен прогон.
Проверка корректности перед замерами
До запуска Tinybench каждый адаптер проходит проверки контрактов. Все адаптеры должны предоставить одинаковый наблюдаемый граф:
Logger,Config,RepoиServiceработают как корневые singleton-сервисы- transient-узлы графа создают новые экземпляры
- каждый скоуп кэширует собственный
ScopedServiceи использует общий корневой сервис логирования - ленивый доступ откладывает получение целевого сервиса
- каждый заявленный API освобождения ресурсов вызывает соответствующий метод scoped-сервиса
Эти проверки не позволяют различиям во времени жизни или идентичности экземпляров попасть в сравнение времени. Сценарии измеряют стоимость операций после успешной проверки адаптера.
Методика измерений
Скрипт публичного прогона устанавливает зависимости без изменения lockfile, проверяет типы в рабочем пространстве бенчмарков, собирает InferDI в production-формате ESM и проверяет на этой сборке контракты адаптеров. Затем запускаются измерения:
- Прогон состоит из восьми раундов по числу участников. Порядок сбалансирован по схеме латинского квадрата.
- Каждый участник один раз занимает каждую позицию запуска и один раз следует за каждым другим участником.
InferDI (fast)запускается раньшеInferDI (default)в четырёх раундах и позже в остальных четырёх. - Каждый участник работает в отдельном новом процессе Node.js. Реализации не делят накопленные данные JIT, встроенные кэши вызовов (inline caches) и историю сборки мусора.
- Tinybench прогревает каждый сценарий 50 ms и измеряет его 100 ms. Один замер включает серию операций; её размер подбирается под сценарий.
- Для каждого участника, сценария и раунда скрипт делит медианную длительность серии на число операций и записывает результат в
ns/op. - Генератор отчёта сводит восемь результатов по раундам к медиане и медианному абсолютному отклонению (MAD).
Меньшее значение ns/op означает более быструю операцию. MAD показывает разброс вокруг медианы, но не задаёт доверительный интервал. Изоляция процессов и сбалансированный порядок уменьшают смещение, однако не исключают влияние планировщика ОС, частоты CPU, решений JIT и момента запуска GC.
Границы сценариев
Каждый сценарий изолирует одну часть работы контейнера. Код приложения сочетает несколько этапов в зависимости от выбранного времени жизни.
| Группа нагрузки | Сценарии | Какая работа контейнера представлена |
|---|---|---|
| Тёплый доступ | Hot singleton resolve, warm scoped resolve, lazy resolve | Чтение закэшированного сервиса или доступ через существующую lazy-обёртку |
| Создание объектов | Transient resolve, deep graph, wide graphs | Промахи кэша, обход зависимостей, подготовка аргументов и вызовы конструкторов |
| Запуск и холодный доступ | Registration, first resolve | Настройка графа и первое заполнение кэшей |
| Жизненный цикл скоупа | Scope creation, first scoped resolve, sync teardown, async teardown | Владение скоупом на время запроса или задачи от создания до очистки |
Хуки подготовки Tinybench создают непрогретые графы и скоупы за пределами измеряемого участка. В сценариях регистрации и получения сервисов очистка тоже выполняется вне замера. Сценарии освобождения ресурсов измеряют саму очистку.
У TypeDI стоит N/A в строке Registration: сопоставимые определения сервисов выполняются декораторами при загрузке модуля, а этот этап не входит в замер регистрации. В строках освобождения ресурсов участвуют библиотеки с эквивалентным публичным контрактом. У InversifyJS стоит N/A; TypeDI участвует в синхронном освобождении, Awilix, TSyringe и Typed Inject в асинхронном. InferDI предоставляет оба варианта.
Публичный результат
Каждая ячейка содержит медиана ± MAD в ns/op. Коэффициент в скобках сравнивает медиану с минимальным значением в этой строке.

| Сценарий | InferDI (fast) | InferDI (default) | InversifyJS | Awilix PROXY | Awilix CLASSIC | TSyringe | TypeDI | Typed Inject |
|---|---|---|---|---|---|---|---|---|
| Получение singleton из кэша | 6.233 ± 0.091 (1.00×) | 6.233 ± 0.000 (1.00×) | 10.954 ± 0.321 (1.76×) | 40.425 ± 0.596 (6.49×) | 41.525 ± 0.367 (6.66×) | 81.354 ± 3.254 (13.05×) | 71.729 ± 0.962 (11.51×) | 48.354 ± 1.374 (7.76×) |
| Получение transient-сервиса | 41.798 ± 2.196 (1.00×) | 45.834 ± 1.284 (1.10×) | 79.566 ± 3.850 (1.90×) | 219.82 ± 8.250 (5.26×) | 227.15 ± 8.068 (5.43×) | 301.40 ± 16.682 (7.21×) | 519.75 ± 5.498 (12.43×) | 138.05 ± 1.464 (3.30×) |
| Глубокий граф (10 уровней) | 344.21 ± 11.460 (1.00×) | 445.50 ± 10.085 (1.29×) | 397.37 ± 7.795 (1.15×) | 1306.3 ± 16.500 (3.79×) | 1190.8 ± 12.375 (3.46×) | 1463.0 ± 34.835 (4.25×) | 4478.8 ± 26.582 (13.01×) | 717.75 ± 3.670 (2.09×) |
| Широкий граф (4 зависимости) | 59.584 ± 1.282 (1.00×) | 71.316 ± 1.284 (1.20×) | 103.22 ± 2.932 (1.73×) | 331.10 ± 6.416 (5.56×) | 302.69 ± 5.498 (5.08×) | 457.97 ± 18.150 (7.69×) | 863.32 ± 12.832 (14.49×) | 197.63 ± 1.468 (3.32×) |
| Широкий граф (10 зависимостей) | 188.37 ± 4.125 (1.00×) | 200.74 ± 4.130 (1.07×) | 394.63 ± 4.580 (2.09×) | 681.08 ± 21.080 (3.62×) | 571.54 ± 12.830 (3.03×) | 979.46 ± 15.125 (5.20×) | 2234.1 ± 25.888 (11.86×) | 321.29 ± 4.585 (1.71×) |
| Регистрация | 2007.5 ± 27.500 (1.00×) | 2158.8 ± 13.750 (1.08×) | 61274.6 ± 1537.7 (30.52×) | 74763.4 ± 1024.4 (37.24×) | 95225.6 ± 467.45 (47.43×) | 3762.9 ± 36.650 (1.87×) | N/A | 3593.3 ± 18.300 (1.79×) |
| Первое получение сервиса | 653.13 ± 24.745 (1.00×) | 794.98 ± 6.190 (1.22×) | 9967.4 ± 551.61 (15.26×) | 2434.9 ± 96.250 (3.73×) | 3044.2 ± 70.360 (4.66×) | 1236.8 ± 25.440 (1.89×) | 3322.9 ± 22.455 (5.09×) | 1275.3 ± 22.917 (1.95×) |
| Создание скоупа | 79.750 ± 2.290 (1.00×) | 83.415 ± 5.505 (1.05×) | 6194.4 ± 167.98 (77.67×) | 1054.4 ± 16.960 (13.22×) | 1031.2 ± 7.790 (12.93×) | 503.26 ± 10.540 (6.31×) | 419.38 ± 3.210 (5.26×) | 164.08 ± 1.835 (2.06×) |
| Первое получение scoped-сервиса | 115.05 ± 1.830 (1.00×) | 128.34 ± 1.835 (1.12×) | 3048.6 ± 32.765 (26.50×) | 352.92 ± 1.835 (3.07×) | 362.54 ± 4.590 (3.15×) | 373.54 ± 11.915 (3.25×) | 296.08 ± 4.125 (2.57×) | 203.05 ± 0.920 (1.76×) |
| Получение scoped-сервиса из кэша | 8.387 ± 0.092 (1.05×) | 8.250 ± 0.046 (1.03×) | 30.204 ± 0.367 (3.79×) | 90.841 ± 1.421 (11.39×) | 95.242 ± 1.421 (11.94×) | 78.375 ± 2.429 (9.83×) | 92.630 ± 0.458 (11.61×) | 7.975 ± 0.092 (1.00×) |
| Синхронное освобождение ресурсов | 99.455 ± 1.370 (1.01×) | 98.085 ± 0.925 (1.00×) | N/A | N/A | N/A | N/A | 124.21 ± 3.665 (1.27×) | N/A |
| Асинхронное освобождение ресурсов | 242.91 ± 3.210 (1.00×) | 249.34 ± 1.840 (1.03×) | N/A | 860.52 ± 20.170 (3.54×) | 886.87 ± 13.750 (3.65×) | 1311.8 ± 10.080 (5.40×) | N/A | 721.42 ± 19.935 (2.97×) |
| Ленивое получение сервиса | 18.837 ± 0.458 (1.00×) | 19.020 ± 0.274 (1.01×) | 64.212 ± 3.621 (3.41×) | 115.68 ± 0.825 (6.14×) | 124.09 ± 3.231 (6.59×) | 155.01 ± 5.271 (8.23×) | 271.06 ± 3.941 (14.39×) | 76.496 ± 0.962 (4.06×) |
Исходные данные
График и таблица построены по файлу public-2026-08-17T16-46-00-483Z.json. В исходном результате доступны восемь раундов, нормализованные измерения, статистика Tinybench, параметры окружения и версии зависимостей. README рабочего пространства бенчмарков описывает каждый сценарий, его аналог в приложении и расчёт отчёта.
Интерпретация результата
Один из режимов InferDI даёт минимальную медиану или делит первое место в 12 из 13 сценариев. У InferDI (fast) такой результат в 11 сценариях. Наиболее заметны различия при регистрации, первом получении сервиса, построении графов, в том числе transient, создании скоупа и первом получении scoped-сервиса. На эти операции влияют плоский реестр InferDI, прямые вызовы конструкторов и короткий путь обработки промаха кэша.
Warm scoped resolve (получение scoped-сервиса из кэша) единственный сценарий, в котором ни один режим InferDI не даёт минимальную медиану. У Typed Inject она составляет 7.975 ns, у InferDI default 8.250 ns, у InferDI fast 8.387 ns. Здесь измеряется одно чтение уже закэшированного значения из существующего скоупа. Создание скоупа, первый промах кэша и освобождение ресурсов в замер не входят, хотя реальному запросу могут потребоваться все эти операции.
Почему fast не показывает меньшее время в каждой строке
fast: true меняет обработку промахов кэша, создание объектов, инвалидацию при регистрации и поиск по родителям в фиксированном дереве скоупов. После прогрева часть сценариев не выполняет эти ветки:
- Получение singleton из кэша возвращает значение через
cache.get(key)до чтения флагаfast. Медиана обоих режимов составляет 6.233 ns. - Получение scoped-сервиса из кэша использует тот же быстрый возврат. У fast медиана 8.387 ns, у default 8.250 ns. Разница 0.137 ns получена на одном и том же участке реализации и сопоставима с указанными MAD.
- Синхронное освобождение ресурсов использует общий код в обоих режимах. Разница медиан 1.370 ns равна MAD fast-режима в этом прогоне.
- При асинхронном освобождении ресурсов и ленивом получении сервиса fast немного быстрее, хотя эти операции после прогрева тоже не выигрывают напрямую от отключения проверок. Такие небольшие различия нужно трактовать столь же осторожно.
В отдельных процессах Node.js один и тот же путь может получить разный JIT-код. Также различаются моменты работы планировщика и сборщика мусора. Порядок по схеме латинского квадрата и медиана восьми раундов уменьшают влияние этих факторов, но не устраняют его. Небольшая разница при чтении scoped-сервиса из кэша или синхронном освобождении ресурсов не доказывает замедление fast-режима. Различия меньше наносекунды или в несколько процентов надёжнее проверять повторными прогонами на машине с контролируемыми условиями.
Крупные различия между fast и default возникают там, где меняется реализация. Медиана default выше в 1.29× для transient-графа глубиной десять уровней, в 1.22× для первого получения сервиса, в 1.20× для широкого графа с четырьмя зависимостями и в 1.12× для первого получения scoped-сервиса. Это согласуется с отключением проверок циклов и времени жизни, а также с прямым поиском регистраций в фиксированном дереве скоупов.
Сопоставляйте сценарий с приложением
В отчёте нет общей оценки: приложения по-разному используют контейнер. Долгоживущий процесс может один раз зарегистрировать граф, а затем в основном читать singleton-сервисы из кэша. HTTP-адаптер на каждый запрос создаёт скоуп, впервые получает scoped-сервисы, затем читает их из кэша и в конце освобождает скоуп. Фоновый обработчик может создавать transient-графы без скоупов.
Не усредняйте относительные коэффициенты разных строк. В сценариях различаются размеры серий операций и участки кода, время которых измеряется. Выберите строки, соответствующие реальному использованию контейнера в вашем приложении, затем профилируйте приложение целиком, включая фреймворк и ввод-вывод.
Сравнение относится к указанным версиям пакетов, адаптерам, тестовым сценариям и машине. У каждой библиотеки своя публичная модель жизненного цикла. N/A означает, что для этой строки нет эквивалентной операции. Бенчмарк измеряет накладные расходы контейнера, а не полное время обработки запроса.
fast: true
Справочное описание аргумента конструктора находится в разделе Опции контейнера. Здесь разобрано влияние этого выбора на производительность.
new Container({ fast: true }) отключает учёт циклов, отслеживание стека singleton-зависимостей и try/finally вокруг защищённого пути получения сервиса. Скоупы обращаются прямо к владельцу реестра, не обходя цепочку родителей. Singleton, полученный у предка, после первого обращения также сохраняется в локальном кэше скоупа. В режиме по умолчанию каждый локальный промах запускает поиск по точной цепочке родителей, поэтому изменения графа остаются видимыми. Fast-контейнеры пропускают защитную инвалидацию при регистрации. Повторяющиеся ссылки на принадлежащие контейнеру экземпляры по-прежнему удаляются при освобождении ресурсов.
Вызов new Container() без опций и явное {fast: false} сохраняют проверки во время выполнения и позволяют менять граф.
Включайте fast: true только после тестов, которые прогнали граф с fast: false. TypeScript не видит singleton-циклы, transient-циклы, динамические ключи, as-приведения типов и фабрики, захватившие внешний контейнер с более широким типом. Регистрируйте каждый ключ один раз в одной линейной цепочке вызовов, завершите регистрацию до первого получения сервиса или создания скоупа, не меняйте активированное дерево и закрывайте дочерние скоупы раньше предков.
После проверки графа {fast: true} может снизить затраты на регистрацию, промахи кэша, создание графов и поиск в скоупах фиксированного дерева. Чтение из прогретого кэша и освобождение ресурсов используют общий код в обоих режимах. Перед выбором режима измерьте операции, которые преобладают в приложении. Для разработки, тестов, горячей перезагрузки и любого дерева, меняющегося после активации, оставьте fast: false.
Детали горячего пути
Создание transient-сервисов
registerClass остаётся основным вариантом для transient-сервисов. Меняйте его только после профилирования графа, который часто получает много разных transient-классов с одинаковым числом зависимостей.
Если профилировщик обнаружил именно такое узкое место в V8, явная фабрика создаст отдельное место вызова конструктора для каждого сервиса:
const container = new Container()
.declareScopeInputs<{ context: RequestContext }>()
.registerClass('schema', Schema, [])
.registerFactory(
'parseRequest',
(c) => new ParseRequest(c.get('context'), c.get('schema')),
['context', 'schema'],
'transient'
)В фабрике приходится повторять зависимости, поэтому используйте её только при измеримом выигрыше. Общая обобщённая вспомогательная функция снова объединит вызовы в одном месте и сведёт оптимизацию на нет.
Представление ключей
Symbol-ключи могут помочь при частом получении сервисов в цикле, потому что Map сравнивает их по идентичности. Строковым ключам нужен хеш, а при коллизии - посимвольное сравнение. В большинстве приложений разница не измеряется, поэтому переходите на symbol-ключи только после сигнала профилировщика.
Запуск локально
cd benchmarks
pnpm install --frozen-lockfile
pnpm run bench:quick # локальная проверка source
pnpm run bench:public # production artifact, новый процесс для каждого subjectРабочее пространство бенчмарков изолировано от корневого рабочего пространства pnpm и имеет собственный lockfile. Методология и фикстуры описаны в benchmarks/README.md.
