The Problem
When running CLI commands (especially simple ones like asyncapi --help, asyncapi --version, or asyncapi config), there is a noticeable delay before the output appears on the terminal.
When profiling module load times in the built codebase (lib/), Node.js spends significant time loading heavy dependencies upfront before the CLI even determines which command to run:
require('@asyncapi/parser') takes ~550 ms.
require('lib/apps/cli/internal/base') takes ~670 ms.
require('lib/domains/services/validation.service') takes ~1,000 ms.
- Running a topic command like
asyncapi config takes ~3.4 seconds.
NOTE: These benchmarks are based on my machine so they may not be universal for everyone.
How were these numbers measured? (Reproducible Benchmarks)
- CLI (
time command):
# Build the lib/ output first
npm run build
# Measure execution time of a topic command
time ./bin/run_bin config
Output:
real 0m3.401s
user 0m1.060s
sys 0m0.235s
Why is this happening?
Looking at the code, a few heavy modules are imported and instantiated at the top level when files are loaded:
src/apps/cli/internal/base.ts
- Imports
@asyncapi/parser at the top level and creates parser = new Parser(); on class definition. Since base.ts is the base class for commands, this runs every time commands are indexed or loaded.
- Command classes (e.g.
src/apps/cli/commands/validate.ts, convert.ts)
- Classes create service instances (like
private validationService = new ValidationService();) directly as class properties. When OCLIF loads the command module, these fields are evaluated right away.
src/domains/services/validation.service.ts
- Top-level imports bring in several schema parsers (Avro, OpenAPI, RAML, Protobuf) and formatters all at once.
Because of this, running basic commands like help or topic loads heavy modules unnecessarily.
Proposed Solution
Instead of loading services at top-level, we can defer loading them until the command's run() method actually executes, or use lazy getters.
Example Changes
-
In src/apps/cli/internal/base.ts:
Convert the parser property to a lazy getter so @asyncapi/parser is only required when this.parser is accessed:
private _parser?: any;
get parser() {
if (!this._parser) {
const { Parser } = require('@asyncapi/parser');
this._parser = new Parser();
}
return this._parser;
}
-
In Command Files (e.g. validate.ts, convert.ts):
Move service instantiation out of class field declarations into run() or a lazy getter:
private _validationService?: ValidationService;
private get validationService(): ValidationService {
if (!this._validationService) {
const { ValidationService } = require('@services/validation.service');
this._validationService = new ValidationService();
}
return this._validationService;
}
-
In Services (validation.service.ts):
Defer loading secondary schema parsers (Avro, Protobuf, RAML) until they are required explicitly.
Expected Results
- Faster startup for simple commands: Commands like
asyncapi config, --help, or --version can run much faster.
- Better shell completion: Keeps tab-completion responsive when developers type
asyncapi <TAB>.
Questions for Maintainers
- Does this lazy-loading approach align with how maintainers want to enhance the performance of the cli?
- Are there specific commands or services where maintainers prefer keeping standard imports vs lazy loading?
- Are there any other things regarding this issue, which maintainers want to be addressed or solved in a different way?
Are you willing to work on this issue?
Yes, I'm willing to submit a PR.
The Problem
When running CLI commands (especially simple ones like
asyncapi --help,asyncapi --version, orasyncapi config), there is a noticeable delay before the output appears on the terminal.When profiling module load times in the built codebase (
lib/), Node.js spends significant time loading heavy dependencies upfront before the CLI even determines which command to run:require('@asyncapi/parser')takes ~550 ms.require('lib/apps/cli/internal/base')takes ~670 ms.require('lib/domains/services/validation.service')takes ~1,000 ms.asyncapi configtakes ~3.4 seconds.NOTE: These benchmarks are based on my machine so they may not be universal for everyone.
How were these numbers measured? (Reproducible Benchmarks)
timecommand):Why is this happening?
Looking at the code, a few heavy modules are imported and instantiated at the top level when files are loaded:
src/apps/cli/internal/base.ts@asyncapi/parserat the top level and createsparser = new Parser();on class definition. Sincebase.tsis the base class for commands, this runs every time commands are indexed or loaded.src/apps/cli/commands/validate.ts,convert.ts)private validationService = new ValidationService();) directly as class properties. When OCLIF loads the command module, these fields are evaluated right away.src/domains/services/validation.service.tsBecause of this, running basic commands like help or topic loads heavy modules unnecessarily.
Proposed Solution
Instead of loading services at top-level, we can defer loading them until the command's
run()method actually executes, or use lazy getters.Example Changes
In
src/apps/cli/internal/base.ts:Convert the
parserproperty to a lazy getter so@asyncapi/parseris only required whenthis.parseris accessed:In Command Files (e.g.
validate.ts,convert.ts):Move service instantiation out of class field declarations into
run()or a lazy getter:In Services (
validation.service.ts):Defer loading secondary schema parsers (Avro, Protobuf, RAML) until they are required explicitly.
Expected Results
asyncapi config,--help, or--versioncan run much faster.asyncapi <TAB>.Questions for Maintainers
Are you willing to work on this issue?
Yes, I'm willing to submit a PR.