NestJS Enterprise Backend APIs · Урок

Модули с ленивой загрузкой и переключатели функций

Загружайте необязательные функциональные модули по требованию с помощью LazyModuleLoader, сокращая время запуска.

Урок 3 из 413 шагов

«Модули с ленивой загрузкой и переключатели функций» — бесплатный урок NestJS Enterprise Backend APIs на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения NestJS Enterprise Backend APIs, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс NestJS Enterprise Backend APIs содержит 4 уроков всего.

Части этого урока еще не переведены и отображаются на английском.

Why Eager Loading Hurts Startup

By default, NestJS instantiates every module in your imports graph at bootstrap. For an enterprise API with dozens of optional features — a PDF exporter, a payment gateway, an AI scoring engine — that means paying the full provider-instantiation and connection-warmup cost before the app even accepts a request.

  • Heavy SDKs (Stripe, AWS, gRPC clients) run their constructors eagerly.
  • Modules a given deployment never uses still load.
  • Cold-start latency grows linearly with the module graph.

The fix: load select feature modules lazily, only when first invoked.

The LazyModuleLoader

Nest ships a built-in LazyModuleLoader (from @nestjs/core). You inject it like any provider, then call load() with a factory that returns the module. Nest registers the module's providers on demand and caches the resulting reference for subsequent calls.

Key traits:

  • Lazy modules are not listed in any imports array.
  • They do not register controllers, resolvers, or enhancers — only providers.
  • The first load() instantiates; later calls return the cached ModuleRef.
import { Injectable } from '@nestjs/common';
import { LazyModuleLoader } from '@nestjs/core';

@Injectable()
export class ReportsService {
  constructor(private readonly lazyModuleLoader: LazyModuleLoader) {}

  async generate(): Promise<void> {
    const { PdfModule } = await import('./pdf/pdf.module');
    const moduleRef = await this.lazyModuleLoader.load(() => PdfModule);
    // moduleRef now exposes PdfModule's providers
  }
}

Resolving a Provider From the Lazy Module

The object returned by load() is a ModuleRef. Use its get() method to pull a concrete provider out of the freshly-loaded module. Because lazy modules are isolated, request a provider only after the module is loaded.

For request-scoped or transient providers use moduleRef.resolve() instead of get().

async generate(): Promise<Buffer> {
  const { PdfModule } = await import('./pdf/pdf.module');
  const moduleRef = await this.lazyModuleLoader.load(() => PdfModule);

  const pdfService = moduleRef.get(PdfService);
  return pdfService.render({ title: 'Invoice' });
}

Dynamic import() Is What Saves the Bytes

The real startup win comes from pairing LazyModuleLoader.load() with a dynamic import(). A static top-level import pulls the module — and its heavy transitive deps — into the bootstrap bundle. A dynamic import() defers that file evaluation until the call runs.

  • Static import { PdfModule } at the top of the file = loaded at startup.
  • await import('./pdf/pdf.module') inside the method = loaded on first use.

So always import the lazy module's file dynamically, never statically.

A Lazy Feature Module Definition

The lazy module itself is an ordinary @Module — there is nothing special in its decorator. What makes it lazy is purely how it is consumed (via LazyModuleLoader rather than an imports array).

Keep its providers self-contained so loading it does not drag in the whole app.

import { Module } from '@nestjs/common';
import { PdfService } from './pdf.service';

@Module({
  providers: [PdfService],
  exports: [PdfService],
})
export class PdfModule {}

Caching Makes Repeated Loads Cheap

Nest internally keeps a registry of already-loaded lazy modules keyed by the factory result. So calling load() repeatedly with the same module class is effectively free after the first hit — no double instantiation, no duplicate connections.

This means you can safely call load() inline in a hot handler without guarding it yourself; the framework deduplicates. The one-time cost is paid on the first request that needs the feature.

Feature Toggles: Gate the Load

Feature toggles and lazy loading are a natural pair. Instead of conditionally registering modules at compile time, you check a flag at runtime and only load() the module when the flag is on. A disabled feature then costs nothing — not even its constructor.

  • Flag from env, config service, or a remote flag provider (LaunchDarkly, Unleash).
  • If the toggle is off, short-circuit before importing.
@Injectable()
export class ExportService {
  constructor(
    private readonly lazyModuleLoader: LazyModuleLoader,
    private readonly flags: FeatureFlagService,
  ) {}

  async export(payload: ExportDto) {
    if (!this.flags.isEnabled('pdf-export')) {
      throw new ForbiddenException('Feature disabled');
    }
    const { PdfModule } = await import('./pdf/pdf.module');
    const ref = await this.lazyModuleLoader.load(() => PdfModule);
    return ref.get(PdfService).render(payload);
  }
}

A Minimal Flag Service (Standalone)

A feature-flag check is just a deterministic lookup. Here is a framework-free version you can reason about and test in isolation — the same logic a Nest FeatureFlagService would wrap. It reads a flag map and falls back to a default when the key is unknown.

class FeatureFlags {
  constructor(private readonly flags: Record<string, boolean>) {}

  isEnabled(key: string, fallback = false): boolean {
    return this.flags[key] ?? fallback;
  }
}

const flags = new FeatureFlags({ 'pdf-export': true, 'ai-scoring': false });

console.log(flags.isEnabled('pdf-export'));   // true
console.log(flags.isEnabled('ai-scoring'));   // false
console.log(flags.isEnabled('unknown', true)); // true (fallback)

Controllers and Enhancers Are Ignored

A critical limitation: when a module is loaded lazily, Nest registers its providers only. It deliberately skips:

  • controllers — no new HTTP routes appear.
  • Global guards, interceptors, pipes, filters declared in the module.
  • GraphQL resolvers.

So a lazy module cannot add endpoints. Expose the feature through a controller in an eagerly-loaded module that delegates to the lazily-loaded provider.

@Controller('reports')
export class ReportsController {
  constructor(private readonly reports: ReportsService) {}

  @Post('pdf')
  async pdf(@Body() dto: ExportDto) {
    // controller is eager; PdfModule is loaded lazily inside the service
    return this.reports.generate(dto);
  }
}

Warming Up vs Lazy: Pick Per Feature

Lazy loading trades a one-time first-request latency spike for a faster, lighter startup. That is the right deal for rarely used, expensive features. For features on the hot path, eager loading (or an explicit warm-up on onApplicationBootstrap) avoids penalizing the first user.

Decision guide:

  • Lazy: heavy SDK, used by <X% of requests, optional per deployment.
  • Eager: core domain, every request, latency-sensitive.
  • Lazy + warm-up: heavy but predictably needed soon after boot.
@Injectable()
export class Warmer implements OnApplicationBootstrap {
  constructor(private readonly lazyModuleLoader: LazyModuleLoader) {}

  async onApplicationBootstrap() {
    if (process.env.PRELOAD_PDF === 'true') {
      const { PdfModule } = await import('./pdf/pdf.module');
      await this.lazyModuleLoader.load(() => PdfModule); // pay cost now, off the request path
    }
  }
}

Measuring the Win

Quantify before and after. Wrap bootstrap timing and the first lazy load() to confirm the trade-off is real for your workload.

  • Startup time should drop by the cumulative constructor + connection cost of the deferred modules.
  • First-call latency for the lazy feature absorbs that cost once.
  • Watch P99 of the first request after deploy — that is where the deferred cost surfaces.

If the lazy feature is hit on nearly every request, the numbers will tell you to switch it back to eager.

const t0 = performance.now();
const { PdfModule } = await import('./pdf/pdf.module');
const ref = await this.lazyModuleLoader.load(() => PdfModule);
this.logger.log(`Lazy PdfModule ready in ${Math.round(performance.now() - t0)}ms`);

Quick Check

You lazily load PdfModule via LazyModuleLoader. PdfModule declares a controller with a @Post('pdf') route. After loading, the route returns 404. What is the correct explanation and fix?

Recap

You learned how to shrink NestJS startup cost with on-demand modules:

  • LazyModuleLoader.load(() => SomeModule) instantiates a module's providers on first use and caches the result.
  • Pair it with a dynamic import() so the module's file (and heavy deps) is never evaluated at bootstrap.
  • Resolve providers via moduleRef.get() (or resolve() for scoped providers).
  • Feature toggles gate the load: a disabled feature costs nothing, not even a constructor.
  • Lazy modules register no controllers, resolvers, or enhancers — delegate from an eager controller.
  • Choose lazy for heavy, rarely-used, optional features; eager (or lazy + warm-up) for hot-path code. Measure startup and first-call latency to confirm the trade-off.
Можно начать бесплатно

Изучай TypeScript с ИИ-репетитором — бесплатно

Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.

Курсы
20
Уроки
76

Часто задаваемые вопросы

Урок «Модули с ленивой загрузкой и переключатели функций» бесплатный?

Да — полный текст урока «Модули с ленивой загрузкой и переключатели функций» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс NestJS Enterprise Backend APIs, подпишись на CoddyKit PRO. Курс NestJS Enterprise Backend APIs содержит 4 уроков всего.

Чему я научусь в уроке «Модули с ленивой загрузкой и переключатели функций»?

Загружайте необязательные функциональные модули по требованию с помощью LazyModuleLoader, сокращая время запуска. Ты практикуешь NestJS Enterprise Backend APIs с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать NestJS Enterprise Backend APIs?

Предыдущий опыт не требуется. NestJS Enterprise Backend APIs на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «Модули с ленивой загрузкой и переключатели функций»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке NestJS Enterprise Backend APIs?

Да. Каждый урок NestJS Enterprise Backend APIs включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Порты и адаптеры для изоляции домена
  2. Динамическая регистрация провайдеров с DiscoveryService
  3. Модули с ленивой загрузкой и переключатели функций
  4. Точки расширения через API ссылок на модули
← Назад к NestJS Enterprise Backend APIs