iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🦥

Trying out Lazy Loading with NestJS LazyModuleLoader

に公開

Using LazyModuleLoader added in NestJS v8, you can delay the loading of a Module (creation of provider instances) until it is actually needed. This speeds up the application startup time and makes it easier to use in serverless container environments like Cloud Run.

While LazyModuleLoader is clearly explained in the official documentation, I couldn't quite visualize its actual behavior just by reading it, so I decided to try it out myself.
https://docs.nestjs.com/fundamentals/lazy-loading-modules

1. Creating the Project

Create a new NestJS project.

npm i -g @nestjs/cli
nest new sample-project

cd sample-project

Once creation is complete, confirm that the application starts up.

$ npm run start:dev

[Nest] 96119  - 2021/11/10 15:39:08     LOG [NestFactory] Starting Nest application...
[Nest] 96119  - 2021/11/10 15:39:08     LOG [InstanceLoader] AppModule dependencies initialized +52ms
[Nest] 96119  - 2021/11/10 15:39:08     LOG [RoutesResolver] AppController {/}: +12ms
[Nest] 96119  - 2021/11/10 15:39:08     LOG [RouterExplorer] Mapped {/, GET} route +4ms
[Nest] 96119  - 2021/11/10 15:39:08     LOG [NestApplication] Nest application successfully started +3ms

Next, we will add the module that we intend to lazy load. (At this stage, it is still a regular module.)

$ nest generate module cat
CREATE src/cat/cat.module.ts (81 bytes)
UPDATE src/app.module.ts (365 bytes)

$ nest generate controller cat
CREATE src/cat/cat.controller.spec.ts (478 bytes)
CREATE src/cat/cat.controller.ts (97 bytes)
UPDATE src/cat/cat.module.ts (166 bytes)

$ nest generate service cat
CREATE src/cat/cat.service.spec.ts (446 bytes)
CREATE src/cat/cat.service.ts (88 bytes)
UPDATE src/cat/cat.module.ts (240 bytes)
src/cat/cat.module.ts
@Module({
  providers: [CatService],
  controllers: [CatController],
})
export class CatModule {}

Add a log to the constructor so that you can see when the CatService instance is created.

src/cat/cat.service.ts
@Injectable()
export class CatService {
  constructor() {
    console.log('init CatService');
  }

  run() {
    return 'nyan';
  }
}

Modify CatController to call CatService.

src/cat/cat.controller.ts
@Controller('cat')
export class CatController {
  constructor(private catService: CatService) {}

  @Get()
  index() {
    return this.catService.run();
  }
}

Now, try starting the application.

$ npm run start:dev

[Nest] 8005  - 2021/11/10 15:54:18     LOG [NestFactory] Starting Nest application...
init CatService
[Nest] 8005  - 2021/11/10 15:54:18     LOG [InstanceLoader] AppModule dependencies initialized +57ms
[Nest] 8005  - 2021/11/10 15:54:18     LOG [InstanceLoader] CatModule dependencies initialized +0ms
[Nest] 8005  - 2021/11/10 15:54:18     LOG [RoutesResolver] AppController {/}: +6ms
[Nest] 8005  - 2021/11/10 15:54:18     LOG [RouterExplorer] Mapped {/, GET} route +4ms
[Nest] 8005  - 2021/11/10 15:54:18     LOG [RoutesResolver] CatController {/cat}: +1ms
[Nest] 8005  - 2021/11/10 15:54:18     LOG [RouterExplorer] Mapped {/cat, GET} route +0ms
[Nest] 8005  - 2021/11/10 15:54:18     LOG [NestApplication] Nest application successfully started +4ms

You can see that the CatService instance was created when the application started.

2. Lazy Loading the Module

Now, let's make the CatService lazy-loaded.

First, move the provider part of CatModule to a newly created CatLazyModule.

src/cat/cat.module.ts
@Module({
  controllers: [CatController],
})
export class CatModule {}
src/cat/cat.lazy.module.ts
@Module({
  providers: [CatService],
})
export class CatLazyModule {}

Next, inject LazyModuleLoader into CatController.
Configure it so that the loading of CatService happens only after the first request arrives.

src/cat/cat.controller.ts
@Controller('cat')
export class CatController {
  private catService: CatService;
  constructor(private lazyModuleLoader: LazyModuleLoader) {}

  @Get()
  async index() {
    await this.lazyInit();
    return this.catService.run();
  }

  async lazyInit() {
    if(this.catService) return; // Do nothing if already initialized

    const { CatLazyModule } = await import('./cat.lazy.module');
    const moduleRef = await this.lazyModuleLoader.load(() => CatLazyModule);

    const { CatService } = await import('./cat.service');
    this.catService = moduleRef.get(CatService);
  }
}

Start the application.

$ npm run start:dev

[Nest] 15415  - 2021/11/10 16:04:25     LOG [NestFactory] Starting Nest application...
[Nest] 15415  - 2021/11/10 16:04:25     LOG [InstanceLoader] AppModule dependencies initialized +40ms
[Nest] 15415  - 2021/11/10 16:04:25     LOG [InstanceLoader] CatModule dependencies initialized +0ms
[Nest] 15415  - 2021/11/10 16:04:25     LOG [RoutesResolver] AppController {/}: +5ms
[Nest] 15415  - 2021/11/10 16:04:25     LOG [RouterExplorer] Mapped {/, GET} route +3ms
[Nest] 15415  - 2021/11/10 16:04:25     LOG [RoutesResolver] CatController {/cat}: +0ms
[Nest] 15415  - 2021/11/10 16:04:25     LOG [RouterExplorer] Mapped {/cat, GET} route +1ms
[Nest] 15415  - 2021/11/10 16:04:25     LOG [NestApplication] Nest application successfully started +2ms

CatService has not been initialized yet.

When you make a request to http://localhost:3000/cat/, the following logs are output:

init CatService
[Nest] 15415  - 2021/11/10 16:04:46     LOG [LazyModuleLoader] CatLazyModule dependencies initialized

At this point, you can confirm that the CatService instance has been created.

3. Other Methods

Although the use case is slightly different, if you only need to create a provider instance when a request arrives, you can also set the scope to REQUEST in the @Injectable parameters. When scope is set to REQUEST, an instance to be injected is created and destroyed for each request.

Assume the source code state is a continuation from step 1.

src/cat/cat.service.ts
@Injectable({ scope: Scope.REQUEST })
export class CatService {
  constructor() {
    console.log('init CatService');
  }

  run() {
    return 'nyan';
  }
}

Start the application.

$ npm run start:dev

[Nest] 36104  - 2021/11/10 16:36:30     LOG [NestFactory] Starting Nest application...
[Nest] 36104  - 2021/11/10 16:36:30     LOG [InstanceLoader] AppModule dependencies initialized +38ms
[Nest] 36104  - 2021/11/10 16:36:30     LOG [InstanceLoader] CatModule dependencies initialized +0ms
[Nest] 36104  - 2021/11/10 16:36:30     LOG [RoutesResolver] AppController {/}: +5ms
[Nest] 36104  - 2021/11/10 16:36:30     LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 36104  - 2021/11/10 16:36:30     LOG [RoutesResolver] CatController {/cat}: +0ms
[Nest] 36104  - 2021/11/10 16:36:30     LOG [RouterExplorer] Mapped {/cat, GET} route +1ms
[Nest] 36104  - 2021/11/10 16:36:30     LOG [NestApplication] Nest application successfully started +2ms

Similar to lazy loading, CatService has not been initialized yet.

When you make a request to http://localhost:3000/cat/, the following log is output:

init CatService

Conclusion

I have confirmed how to lazy load a Module. I still don't quite understand the benefits of the Module (DI) system in this context, so I sometimes wonder if it wouldn't be easier to just not use a Module at all? (For example, just using a regular new in the Controller's constructor?) If you have any opinions, please let me know!

GitHubで編集を提案

Discussion