DI Benchmark: Vanilla, RegistryComposer, typed-inject, tsyringe, inversify, nest.js
Tags: benchmark, typescript, node, dependencyinjection
Published: 2023-02-26 01:21:49

---
title: DI Benchmark: Vanilla, RegistryComposer, typed-inject, tsyringe, inversify, nest.js
published: true
date: 2023-02-25 15:43:00 UTC
tags: benchmark, typescript, node, dependencyinjection
canonical_url: https://blog.vady.dev/di-benchmark-vanilla-registrycomposer-typed-inject-tsyringe-inversify-nestjs
---

In the previous articles, I've introduced a [framework-less and clean way to perform Dependency Injection in TypeScript](https://dev.to/vad3x/typesafe-almost-zero-cost-dependency-injection-in-typescript-112).

The approach has multiple pros such as type-safety, zero-cost overhead, and others.

However, the one missing piece I had was - benchmarking.

In this article I'm going to compare the performance of the following DI approaches/libs:

- Vanilla TypeScript

- [registry-composer](https://dev.to/vad3x/typesafe-almost-zero-cost-dependency-injection-in-typescript-112)

- [typed-inject](https://github.com/nicojs/typed-inject)

- [tsyringe](https://github.com/microsoft/tsyringe)

- [inversify](https://inversify.io/)

- [nest.js](https://nestjs.com/)

## Application

For the test I'm defining the following application dependency tree:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677266964218/de5c428e-f9dd-47be-8eb9-af190e46c0c2.png)

The implementation of the functions and services does not really matter for our tests, since we only test the dependency resolution performance. So the most important thing here is the depth of the graph.

In the application to resolve `findUserReviewedProducts` the server needs to resolve all `findUserByUsername`, `findUserReviews`, `findProductById`, and also their dependencies: `UserStore`, `ProductStore`, and `UserReviewStore`, etc.

So, to resolve `findUserReviewedProducts` - 9 other dependencies need to be resolved.

> NOTE: For fare comparison, all dependencies' lifetimes are `Singleton`, so once one is resolved, it can be cached for the whole application lifetime.

## Competitors

### Vanilla TypeScript

Typesafe implicit allocation of the dependency graph.

So the code looks like this:

```ts
// ...
const productStore = new MysqlProductStore(getMysqlClient);
const userStore = new RedisUserStore(getRedisClient);
const userReviewStore = new DynamodbUserReviewStore(getDynamodbClient);

const { findProductById } = new ImplFindProductById(productStore);
const { findUserByUsername } = new ImplFindUserByUsername(userStore);
const { findUserReviews } = new ImplFindUserReviews(userReviewStore);

const { findUserReviewedProducts } = new ImplFindUserReviewedProducts(
  findUserByUsername,
  findUserReviews,
  findProductById
);

return Object.freeze({
  getMysqlClient,
  getRedisClient,
  getDynamodbClient,
  findProductById,
  productStore,
  userStore,
  userReviewStore,
  findUserByUsername,
  findUserReviews,
  findUserReviewedProducts,
});

```

> NOTE: I'm using `Vanilla TypeScript` as a baseline for all the benchmarking since it's the most performant solution.

### Registry Composer

Typesafe dependency injection approach, that does not require to use of any libraries. Very similar to vanilla TS, but the main benefit - it is helping to organize the registration complexity.

```ts
new RegistryComposer()
  .add(implGetMysqlClient())
  .add(implGetRedisClient())
  .add(implGetDynamodbClient())
  .add(mysqlProductStore())
  .add(redisUserStore())
  .add(dynamodbUserReviewStore())
  .add(implFindProductById())
  .add(implFindUserByUsername())
  .add(implFindUserReviews())
  .add(implFindUserReviewedProducts())
  .compose();

```

### typed-inject

Typesafe dependency injection framework for TypeScript. Conceptually it's very similar to `Registry Composer` except, it requires adding specific `public static inject` field to every class. Registration of functions is also not very [pretty](https://github.com/nicojs/typed-inject#-lifecycle-control).

```ts
createInjector()
  // ...
  .provideClass("ProductStore", DecoratedMysqlProductStore)
  .provideClass("UserReviewStore", DecoratedDynamodbUserReviewStore)
  .provideClass("UserStore", DecoratedRedisUserStore)
  .provideClass("DecoratedImplFindProductById", DecoratedImplFindProductById)
  .provideFactory(
    "FindProductById",
    provideFn(["DecoratedImplFindProductById"], "findProductById")
  )
  .provideClass(
    "DecoratedImplFindUserByUsername",
    DecoratedImplFindUserByUsername
  );
// ...

```

### tsyringe

Lightweight dependency injection container for TypeScript. The container is decorator-driven and not typesafe.

```ts
// ...

container.register("ProductStore", {
  useClass: DecoratedMysqlProductStore,
});

container.register("UserReviewStore", {
  useClass: DecoratedDynamodbUserReviewStore,
});

container.register("UserStore", {
  useClass: DecoratedRedisUserStore,
});

// ...

```

### inversify

Another popular decorator-driven and not typesafe dependency injection container for TypeScript.

```ts
const container = new Container({
  defaultScope: "Singleton",
});

// ...
container.bind("ProductStore").to(DecoratedMysqlProductStore);
container.bind("UserReviewStore").to(DecoratedDynamodbUserReviewStore);
container.bind("UserStore").to(DecoratedRedisUserStore);
// ...

```

### nest.js

Unlike others, `nest.js` is not just a DI library, it's a complete framework for building backend services. The DI system is very similar to [Angular Modules](https://angular.io/guide/architecture-modules).

```ts
@Module({
  providers: [
    DecoratedImplFindProductById,
    DecoratedImplFindUserByUsername,
    DecoratedImplFindUserReviewedProducts,
    DecoratedImplFindUserReviews,
    // ...
    {
      provide: "FindUserReviews",
      useFactory: ({ findUserReviews }: DecoratedImplFindUserReviews) => {
        return findUserReviews;
      },
      inject: [DecoratedImplFindUserReviews],
    },
    {
      provide: "ProductStore",
      useClass: DecoratedMysqlProductStore,
    },
    {
      provide: "UserReviewStore",
      useClass: DecoratedDynamodbUserReviewStore,
    },
    {
      provide: "UserStore",
      useClass: DecoratedRedisUserStore,
    },
    // ...
  ],
})
export class RootModule {}

```

> NOTE: nest.js is a little out of scope since it doesn't expose it's DI container explicitly. But I've decided to test it as well since it's a very popular solution.

## Benchmark Suites

For the benchmarking, I'm using the [benchmark.js](https://benchmarkjs.com/) library with the default config.

There are 3 different benchmarks I'm performing:

### Cold Start

The Cold Start suite covers the case when you just start an application. To do so, we need to skip caching when we start each test. It's achieved by using a helper function `requireUncached`.

```ts
function requireUncached(module: string) {
  delete require.cache[require.resolve(module)];
  return require(module);
}

```

For example for `inversify` for every test, I do: Similarly, for `nestjs`:

```ts
requireUncached("reflect-metadata");
requireUncached("@nestjs/core");
requireUncached("@nestjs/common");

```

### Resolution

This suite verifies the performance of service resolution only. Meaning, the cold start happens on the test startup and does not affect the metrics.

For example, the code for the _tsyringe_ looks like this:

```ts
container.resolve("FindUserReviewedProducts");

```

### Cold Start + Resolution

The last suite is a combination of both: the `cold start` and `resolution`.

```ts
requireUncached("reflect-metadata");
requireUncached("inversify");

...

createContainer().get("FindUserReviewedProducts");

```

## Scenario Test Cases

Each scenario contains the following test cases:

- _vanilla_ - a test case for a state object created by implicit allocation

- _registry-composer_ - a test case for a state object created by using `RegistryComposer`

- _typed-inject_ - a test case for a DI container created by using `typed-inject` library

- _tsyringe_ - a test case for a DI container created by using `tsyringe` library

- _inversify_ - a test case for a DI container created by using `inversify` library

- _nest_ - a test case for a DI container created by using `nest.js` library

- _tsyringe#frozen_ - a test case for a state object created from a DI container created by using `tsyringe` library

- _inversify#frozen_ - a test case for a state object created from a DI container created by using `inversify` library

## Benchmarking

### Benchmarking / Cold Start

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677271901140/417a7731-9310-4039-8ca1-b3d6682e506d.png)

What we can see from the chart: the transpile-time solutions are 3x faster than the runtime containers.

> NOTE: even transpile-time solutions are 3x times faster, it does not mean your app will start 3x times faster, since we only compare DI framework timing, excluding any extra dependency your app might have.

### Benchmarking / Resolution

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677267048536/e7d1137e-a1c6-43e2-846b-d4e992d8295e.png)

Since benchmarking all transpile-time and _frozen_ solutions is accessing a field by name, it's very fast. We can see that all the solutions can perform ~950M op/sec. While runtime container solutions perform in the range of 1M-6M op/sec, which is 150x times slower.

### Benchmarking / Cold Start + Resolution

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677271910290/02587338-b692-43e0-a04b-f66fd06c3f8d.png)

Similarly to just the _Cold Start_ test, the transpile-time solutions are 3x faster than the runtime containers.

## Conclusion

The results I got are pretty obvious. The runtime container adds overhead, compared to transpile-time solutions. It's especially clear for _Resolution_ tests since the runtime is 150x times different. The runtime containers are less performant and not typesafe, but provide an easier way to manage services lifetimes. While the transpile-time solutions are typesafe, very fast, and make the application code framework agnostic, but require more care at the code design stage (especially for the [_scope_ lifetime dependencies](https://dev.to/vad3x/step-by-step-guide-of-implementing-scoped-like-dependencies-using-asynclocalstorage-within-fastify-3cg9)). At the end of the day, it's up to you what solution to use, since each has its pros and cons.

> A full example source code can be found [here](https://github.com/vad3x/typesafe-dependency-injection-in-typescript-samples/tree/main/samples/part6-benchmark)