Skip to content

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

Записанный результат

В публичном прогоне от 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 и проверяет на этой сборке контракты адаптеров. Затем запускаются измерения:

  1. Прогон состоит из восьми раундов по числу участников. Порядок сбалансирован по схеме латинского квадрата.
  2. Каждый участник один раз занимает каждую позицию запуска и один раз следует за каждым другим участником. InferDI (fast) запускается раньше InferDI (default) в четырёх раундах и позже в остальных четырёх.
  3. Каждый участник работает в отдельном новом процессе Node.js. Реализации не делят накопленные данные JIT, встроенные кэши вызовов (inline caches) и историю сборки мусора.
  4. Tinybench прогревает каждый сценарий 50 ms и измеряет его 100 ms. Один замер включает серию операций; её размер подбирается под сценарий.
  5. Для каждого участника, сценария и раунда скрипт делит медианную длительность серии на число операций и записывает результат в ns/op.
  6. Генератор отчёта сводит восемь результатов по раундам к медиане и медианному абсолютному отклонению (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)InversifyJSAwilix PROXYAwilix CLASSICTSyringeTypeDITyped 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/A3593.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/AN/AN/AN/A124.21 ± 3.665 (1.27×)N/A
Асинхронное освобождение ресурсов242.91 ± 3.210 (1.00×)249.34 ± 1.840 (1.03×)N/A860.52 ± 20.170 (3.54×)886.87 ± 13.750 (3.65×)1311.8 ± 10.080 (5.40×)N/A721.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, явная фабрика создаст отдельное место вызова конструктора для каждого сервиса:

ts
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-ключи только после сигнала профилировщика.

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

bash
cd benchmarks
pnpm install --frozen-lockfile
pnpm run bench:quick   # локальная проверка source
pnpm run bench:public  # production artifact, новый процесс для каждого subject

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