Performance rules
7 rules that detect performance anti-patterns and dead code.
| Rule | Severity | What it catches |
|---|---|---|
no-sync-io | warning | readFileSync, writeFileSync, etc. |
no-blocking-constructor | warning | Loops in Injectable/Controller constructors |
no-dynamic-require | warning | require() with non-literal argument |
no-unused-providers | warning | Provider never injected and no self-activating decorators |
no-request-scope-abuse | warning | Any Scope.REQUEST in a scanned file |
no-unused-module-exports | info | Export that no importing module injects |
no-orphan-modules | info | Module 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:
| Role | Name suffix |
|---|---|
| Request pipeline | Guard, Interceptor, Filter, Pipe, Middleware |
| Authentication | Strategy |
| Events and queues | Subscriber, Listener, Processor, Consumer |
| Background work | Worker, Scheduler, Cron |
| Health checks | HealthIndicator |
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 {}