Skip to content

CLI parser-only paths (--help, --version, completion) pay full app-bootstrap import cost #27799

@danfry1

Description

@danfry1

Description

Every opencode invocation eagerly loads all 22 top-level command modules — even when the user only needed --help, --version, or shell tab completion. The chain runs through effect-cmd.ts, which transitively imports Effect, Provider, Session, Tool, InstanceStore, AppRuntime, etc. The result is that parser-only paths pay the full app-bootstrap import cost.

This is particularly bad for shell tab completion, which fires on every Tab keystroke and pays this cost each time.

Measurements

Compiled binary on dev (current HEAD), warm runs, median of 10:

Command Time
opencode --help 213ms
opencode --version 193ms
opencode --get-yargs-completions … 192ms
opencode db --help 199ms
opencode mcp --help 195ms

For reference, bun -e '0' cold starts in ~90ms on the same machine, so the bulk of these timings is module evaluation rather than process startup.

Root cause

src/index.ts does e.g. import { RunCommand } from "./cli/cmd/run" synchronously for every command. Loading any of these triggers effect-cmd.ts, which imports Instance / InstanceStore / AppRuntime at the top level — about 500ms of evaluation in dev mode (smaller in the compiled binary, but the proportional cost is similar).

For commands that don't need the full graph (everything that ends at yargs parsing), this work is wasted.

Proposed approach

A small lazy() wrapper for top-level yargs command registration:

  • Registers command, aliases, describe, deprecated synchronously so --help, completion, and argv matching work without touching the implementation module.
  • Dynamic-imports the real CommandModule only when builder or handler actually fires (yargs supports async builders).
  • One shared cached import() per command across builder + handler calls.
  • Wraps loader rejections with the command name so a missing compiled-binary chunk surfaces a useful error.

The default $0 [project] command can't be fully lazy (yargs renders its option spec inline in top-level help), so its spec is extracted into a small shared module that src/index.ts and src/cli/cmd/tui/thread.ts both consume.

Also defers a few other entrypoint imports (Log, Installation, Heap, NamedError, FormatError, drizzle + Database + JsonMigration for the first-run migration) to their use sites.

Expected impact

With a working prototype on a local branch, the same compiled-binary medians become:

Command Before After Change
opencode --help 213ms 69ms −68%
opencode --version 193ms 42ms −78%
opencode --get-yargs-completions … 192ms 42ms −78%
opencode db --help 199ms 63ms −68%
opencode mcp --help 195ms 148ms −24%

Top-level --help output is byte-identical to baseline. No behaviour change for actually running commands — middleware still initialises logging / migration as before once a real command dispatches.

Out of scope

This issue / PR is narrowly about CLI parser-only paths. It does not address full command-execution latency after middleware boot, MCP polling (#27477), file watcher work, or any of the other open perf issues.

Would like to PR

I have a working patch with tests (lazy helper unit tests + a drift guard for the shared $0 spec) and a re-built compiled binary verifying the numbers above. Happy to open it once this issue is triaged.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions