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.
Description
Every
opencodeinvocation eagerly loads all 22 top-level command modules — even when the user only needed--help,--version, or shell tab completion. The chain runs througheffect-cmd.ts, which transitively importsEffect,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:opencode --helpopencode --versionopencode --get-yargs-completions …opencode db --helpopencode mcp --helpFor 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.tsdoes e.g.import { RunCommand } from "./cli/cmd/run"synchronously for every command. Loading any of these triggerseffect-cmd.ts, which importsInstance/InstanceStore/AppRuntimeat 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:command,aliases,describe,deprecatedsynchronously so--help, completion, and argv matching work without touching the implementation module.CommandModuleonly whenbuilderorhandleractually fires (yargs supports async builders).import()per command across builder + handler calls.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 thatsrc/index.tsandsrc/cli/cmd/tui/thread.tsboth consume.Also defers a few other entrypoint imports (
Log,Installation,Heap,NamedError,FormatError,drizzle+Database+JsonMigrationfor the first-run migration) to their use sites.Expected impact
With a working prototype on a local branch, the same compiled-binary medians become:
opencode --helpopencode --versionopencode --get-yargs-completions …opencode db --helpopencode mcp --helpTop-level
--helpoutput 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
$0spec) and a re-built compiled binary verifying the numbers above. Happy to open it once this issue is triaged.