Performance rules

7 rules that detect performance anti-patterns and dead code.

RuleSeverityWhat it catches
no-sync-iowarningreadFileSync, writeFileSync, etc.
no-blocking-constructorwarningLoops in Injectable/Controller constructors
no-dynamic-requirewarningrequire() with non-literal argument
no-unused-providerswarningProvider never injected and no self-activating decorators
no-request-scope-abusewarningAny Scope.REQUEST in a scanned file
no-unused-module-exportsinfoExport that no importing module injects
no-orphan-modulesinfoModule never imported by any other module

no-sync-io

Detects synchronous file system calls like readFileSync, writeFileSync, existsSync, etc.

Why: Sync I/O blocks the Node.js event loop. In a server context, this means no other requests can be processed while the file operation completes. Use async alternatives.

@Injectable()
export class ConfigService {
  loadConfig() {
    const data = readFileSync('config.json', 'utf-8');
    return JSON.parse(data);
  }
}

no-blocking-constructor

Detects loops inside Injectable/Controller constructors.

Why: Constructors run during module initialization. Blocking operations (loops over large datasets) delay application startup and can cause timeouts. Constructors cannot be async, so asynchronous work should always use lifecycle hooks.

@Injectable()
export class CacheService {
  constructor() {
    // Blocks startup
    for (let i = 0; i < 10000; i++) {
      this.cache.set(i, computeExpensiveValue(i));
    }
  }
}

no-dynamic-require

Detects require() calls with non-literal (dynamic) arguments.

Why: A dynamic require() defeats bundler static analysis and can load an unexpected module. When the argument comes from user input, it is also a security risk.

@Injectable()
export class PluginLoader {
  load(name: string) {
    return require(`./plugins/${name}`);
  }
}

no-unused-providers

Scope: Project

Detects @Injectable() providers that are never injected by any other provider or controller.

Why: Unused providers are dead code. They add to module initialization time and obscure the dependency graph.

// Never injected anywhere
@Injectable()
export class LegacyService {
  doOldStuff() { /* ... */ }
}
 
@Module({ providers: [LegacyService, UserService] })
export class UserModule {}

Note: The rule skips a provider whose class name ends in one of these suffixes:

RoleName suffix
Request pipelineGuard, Interceptor, Filter, Pipe, Middleware
AuthenticationStrategy
Events and queuesSubscriber, Listener, Processor, Consumer
Background workWorker, Scheduler, Cron
Health checksHealthIndicator

It also skips self-activating providers. A provider is self-activating when it carries a decorator such as @Cron, @OnEvent, or @Process. Implementing a framework contract such as OnModuleInit or CanActivate counts too.

A provider named in any module's exports array is skipped as well, because a consumer outside the scan may inject it. The exports of a DynamicModule a static method returns count too.


no-request-scope-abuse

Detects Scope.REQUEST, which creates a new instance per HTTP request.

Why: Request-scoped providers disable singleton optimization. Every provider in the dependency chain also becomes request-scoped, which costs real time on high-traffic endpoints.

Note: There is no frequency threshold. Every Scope.REQUEST property access in a scanned file is reported, including one in a plain options object outside a provider class.

@Injectable({ scope: Scope.REQUEST })
export class UserService {
  // New instance created for EVERY request
}

no-unused-module-exports

Scope: Project

Detects a provider a module exports that no importing module injects.

Why: An export nothing injects suggests the module boundary is not being used as intended. Either the export is dead code, or the consuming module never took the dependency. Inject it, or drop it from exports.

Note: The rule needs at least one importing module. A module nothing imports is skipped here and reported by no-orphan-modules instead. A @Global() module counts every other module as an importer.

To find a consumer, the rule reads the constructor parameters and @Inject() tokens of every provider and controller in the importing modules. Providers the importing module registers through a DynamicModule count too.

// shared.module.ts
@Module({ providers: [SharedService], exports: [SharedService] })
export class SharedModule {}
 
// user.module.ts: imports SharedModule
@Module({ imports: [SharedModule], providers: [UserService] })
export class UserModule {}
 
// user.service.ts: nothing injects SharedService
@Injectable()
export class UserService {
  constructor(private readonly userRepo: UserRepository) {}
}

no-orphan-modules

Scope: Project

Detects modules that are never imported by any other module.

Why: An orphan module (other than the root AppModule) is likely dead code or a module that was accidentally disconnected from the module tree.

What counts as a root: an application entry point is never flagged. That covers AppModule and any module declared in an app.module.ts or root.module.ts file.

It also covers a module handed to NestFactory.create() or to nest-commander's CommandFactory.run(). The call can be indirect, through a bootstrap helper whose implementation reaches either factory, such as a shared standaloneBootstrap(RootModule).

Monorepos: diagnostics are checked against the merged workspace graph. A module imported by another sub-project is not an orphan, even when its own project never imports it. That includes an import through a workspace package specifier like @myorg/shared.

// analytics.module.ts: never imported anywhere
@Module({
  providers: [AnalyticsService],
})
export class AnalyticsModule {}