Skip to content

perf(query): lazy-load the MCPServer re-export (14% faster query startup) - #2920

Closed
johnhldiver-beep wants to merge 1 commit into
colbymchenry:mainfrom
johnhldiver-beep:perf/query-startup-lazy-load
Closed

johnhldiver-beep wants to merge 1 commit into
colbymchenry:mainfrom
johnhldiver-beep:perf/query-startup-lazy-load

Conversation

@johnhldiver-beep

Copy link
Copy Markdown

Summary

src/index.ts re-exported MCPServer statically, so every consumer of the package entry — including codegraph query, which reaches the entry through loadCodeGraph() — paid for the whole MCP server module graph (daemon, proxy, engine, session, writer-lock, the 8.3k-line tools module) at startup, although no query path touches it.

This makes the re-export lazy: ./mcp is loaded by a CommonJS getter on first access to MCPServer. The value, the type surface (export const + export type), and the ESM named-export interop are unchanged; cjs-module-lexer interop is preserved because the getter is emitted in the exact shape tsc uses for a static re-export. mcp/version stays eager — it is dependency-free and indexAll() records the package version into the index. codegraph serve/the daemon already used await import('../mcp/index'), and there were no internal call sites for the re-export.

Benchmark

Full method, before/after tables and module counts: docs/benchmarks/query-startup-lazy-mcp.md. A/B alternating runs, n=30 per build, shared machine:

Configuration before (mean) after (mean) delta
shipped (query "Handler" --json) 3454 ms 2957 ms −14.4%
single process (CODEGRAPH_WASM_RELAUNCHED=1) 3026 ms 2538 ms −16.1%

Module graph of a query run: 252 → 227 modules (−25); require('dist/index.js'): 236 (26 mcp/*) → 199 (1 mcp/*).

Tests

  • npx tsc --noEmit — clean.
  • npm run build — clean.
  • New __tests__/lazy-mcp-export.test.ts (3 tests) pins the contract: nothing from the MCP server graph loads until MCPServer is touched, the lazy value is identical to dist/mcp's class, ESM import { MCPServer } still works, and a spawned codegraph query loads only the CLI's small support modules. The two laziness tests fail against a build of the pre-change source. Together with default-export-declaration and cli-index-explicit-path: 12/12.
  • Full npm test: 748/766 files passed, 17 skipped; 8557 tests passed, 408 skipped. One file (react-router.test.ts) hit a beforeAll hook timeout under the 4-worker parallel run while the OS watch/file limit was exhausted (File watcher disabled … too many open files); it passes 70/70 when re-run alone, and is unrelated to this change.

Related: dependency upgrades in #2885, mcp/query-pool in #2862 and init in #2888 are separate efforts; nothing here overlaps them.

`src/index.ts` statically re-exported MCPServer from './mcp', so every
consumer of the entry -- including `codegraph query` through
loadCodeGraph() -- pulled the whole MCP server module graph (daemon,
proxy, engine, session, writer-lock, tools) into startup, though no
query path touches it.

Replace the static re-export with a lazy getter: the same value and type
surface, the same ESM named-export interop (the getter is written in the
exact shape cjs-module-lexer recognizes), and `./mcp` loads on first
access. `mcp/version` stays eager (dependency-free, used by indexAll()).

A/B benchmark (n=30, alternating): shipped query mean 3454ms -> 2957ms
(-14.4%); single process 3026ms -> 2538ms (-16.1%); module graph
252 -> 227. Numbers and method in
docs/benchmarks/query-startup-lazy-mcp.md. New tests lock in the
laziness contract from CJS, ESM and the spawned CLI.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants