Skip to content

[Proposal] Improve CLI startup speed by lazy-loading heavy services and dependencies #2269

Description

@devxdh

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)

  1. 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:

  1. 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.
  2. 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.
  3. 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

  1. 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;
    }
  2. 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;
    }
  3. 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

  1. Does this lazy-loading approach align with how maintainers want to enhance the performance of the cli?
  2. Are there specific commands or services where maintainers prefer keeping standard imports vs lazy loading?
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    To Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions