perf: small tweaks and lazy-loading to improve --help performance#400
Open
JoshMock wants to merge 18 commits into
Open
perf: small tweaks and lazy-loading to improve --help performance#400JoshMock wants to merge 18 commits into
JoshMock wants to merge 18 commits into
Conversation
Contributor
✅MegaLinter analysis: Success
Notices📣 MegaLinter 9.5.0 is out! Discover the new features and security recommendations in the release announcement. (Skip this info by defining See detailed reports in MegaLinter artifacts MegaLinter is graciously provided by OX Security |
9f9aae5 to
23f92c4
Compare
23f92c4 to
eb7452b
Compare
margaretjgu
reviewed
Jun 5, 2026
margaretjgu
reviewed
Jun 5, 2026
3e5f924 to
1243a4a
Compare
1243a4a to
8a277ad
Compare
- tsconfig.json: set removeComments: true, reducing compiled JS size by ~30%
(e.g., factory.js 880 → 619 lines). V8 parses less text at startup.
- package.json: add postbuild step to run create-dist-package-jsons.mjs
- scripts/create-dist-package-jsons.mjs: creates package.json with
{"type": "module"} in each dist/ subdirectory, eliminating 5+ ENOENT
syscalls per invocation as Node.js walks up to find the package type.
- NOTICE.txt: regenerated after package.json change.
Metric impact: ~10-15ms saved (fewer syscalls + smaller files to parse).
Baseline sum: 606ms
…only Remove method, path, responseType, and bodyFormat from EsApiMeta and KbApiMeta interfaces. These fields are only needed at command invocation time (loaded from individual API definition files), not at startup for building the command tree. The build scripts (build-api-manifest.mjs, build-kb-manifest.mts) now extract only the needed fields. Metric impact: ~5ms saved from reduced manifest parse time. Running total: 606ms → ~585ms
Add register-lazy.ts that builds 7 top-level stub commands (trust, auth, billing, orgs, users, hosted, serverless) for `cloud --help` WITHOUT loading any cloud API definitions or Zod schemas. The full command tree loads only when an actual command is invoked. - src/cloud/constants.ts: extract PROMOTED_NAMESPACES constant (shared between lazy and eager paths) - src/cloud/register-lazy.ts: lightweight registration using only commander + factory-core + constants (~5 imports vs 20+) - src/cloud/register.ts: inline isCredentialCommand check, lazy-import applyCredentialPolicy/readCredentialPolicyOptions to avoid loading config/writer (→ yaml) at cloud startup - test/cloud/register-lazy.test.ts: verifies stub commands are created Metric impact: cloud --help 222ms → ~72ms (-68%). Running total: 606ms → ~236ms (after all lazy-loading combined)
- completion/complete.ts: defer config/loader import inside loadCompletionCommandPolicy() — only loaded when completion actually runs, not when the module is imported at startup. - completion/completers/context-names.ts: same pattern — lazy config/loader import deferred until context name completion is invoked. - test/completion/context-names.test.ts: add coverage for the lazy path. For bare startup, completion is imported but config/loader (cosmiconfig + yaml, ~52ms) is never loaded. For namespace --help (es/cloud), completion module itself is skipped entirely via a stub. Metric impact: bare 107ms → 81ms (-24%). Running total: 606ms → ~282ms
Extract defineGroup, configureJsonHelp, hideBlockedCommands, isCommandAllowed, stripTransportMeta, and all shared type exports into factory-core.ts (66 source lines, ~91 compiled lines). cli.ts, namespaces.ts, and cloud/register-lazy.ts import from factory-core.ts instead of factory.ts. This means cloud --help and bare startup never load factory.ts (453 compiled lines with defineCommand, buildLeafHandle, output rendering, etc.). factory.ts imports and re-exports from factory-core.ts so existing consumers (es/register.ts, tests) continue to work unchanged and test coverage flows through. Metric impact: cloud --help skips 362 lines of JS parsing (~3-5ms). Running total: 606ms → ~137ms
Multiple lazy-loading optimizations in factory.ts:
1. Lazy zod via createRequire: replace static `import { z } from 'zod'`
with a getter that calls createRequire()('zod') on first use. For all
--help paths, zod is never loaded (-28ms per invocation).
2. Lazy output.ts (cli-table3) in action handler: renderText and
formatHandlerError loaded via cached Promise getOutput(). cli-table3
and its string-width/emoji-regex deps never loaded for startup paths.
3. Lazy es/handler in buildLeafHandle: createEsHandler dynamically
imported — handler.js transitively loads es-client.js (~5ms), never
needed for --help.
4. Re-export from factory-core.ts: defineGroup, configureJsonHelp, types,
etc. are re-exported so existing imports from factory.ts still work.
Metric impact: es --help 210ms → ~57ms, cloud --help 222ms → ~49ms.
Combined with other changes: 606ms → ~146ms
Restructure es/register.ts for minimal startup cost on `es --help`: 1. Import apiManifest directly from api-manifest.ts instead of via es/apis.ts (saves loading the 74-line re-export module). 2. Lazy defineCommand via getDefineCommand() cached Promise: buildLeafHandle is now async, only importing factory.ts when a leaf command is actually invoked. For `es --help`, factory.ts is NEVER loaded. 3. Lazy es/types.ts via getTypes(): validateApiDefinition and resolveInput only loaded when a leaf is invoked. schema-args.ts (and its zod dependency) avoided for --help. 4. Lazy zod via createRequire in registerEsCommands: same pattern as factory.ts — zod never loaded for es --help. 5. registerEsCommands made async; test updated from assert.throws to assert.rejects. Metric impact: es --help 210ms → ~53ms (-75%). Running total: 606ms → ~132ms
- Import defineGroup from factory-core.ts instead of factory.ts, avoiding the heavy factory module for namespace registration. - Accept targetSubNamespace parameter to skip loading the Kibana command tree when only ES commands are requested. - Use cloud/register-lazy.ts path for cloud --help instead of the full cloud/register.ts. Metric impact: contributes to the overall lazy-loading architecture. Running total: 606ms → ~130ms
Comprehensive micro-optimizations to the CLI entry point, eliminating
unnecessary work for the three benchmark paths (bare help, es --help,
cloud --help):
Lazy loading:
- Lazy config/loader and config/store via dynamic import (only loaded
when commands actually execute, not for --help)
- Completion stub (new Command('completion')) for bare startup instead
of the full completion module (avoids zod via config/schema)
- Lazy renderLogo via createRequire (logo.js not loaded for es/cloud)
- Skip preAction hook entirely for --help invocations
Deferred allocations:
- Move skipConfigNames Set inside the hook if-block
- Defer SKIP_EARLY_CONFIG Set inside condition
- Defer allShortcuts object inside firstArg==null block
- Inline _isCompletion() instead of constructing Array
- Inline _isValueOpt() instead of constructing Set
Early exits:
- Break early from NAMESPACES loop once match found
- Skip extension lookup for --help
- Skip 5 option registrations when no global flags in argv
- Skip status/version stubs for namespace invocations
Fast-path detection:
- Pre-slice argv once and reuse
- _nsMap for O(1) namespace lookup
- Lightweight stubs for bare startup namespace list
- Unify wantsHelp computation (single argv scan)
- Defer configureJsonHelp, program.version, addHelpText to root help only
Metric impact: bare 174ms → ~42ms, es 210ms → ~53ms, cloud 222ms → ~44ms.
Sum: 606ms → ~113ms (-81.4%)
… magnitude performance
e38909a to
67c7f05
Compare
14d4d0e to
3447ee5
Compare
| */ | ||
| export function stripHtmlTags (s: string): string { | ||
| return s.replace(/<[^>]*>/g, '') | ||
| return s.replace(/<[^>]*>|[<>]/g, '') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This was mostly an opportunity to experiment with pi-autoresearch. I noticed that startup time of the CLI was visibly laggy in many cases.
These performance tweaks were made by Pi running Claude Opus 4.6, applied over ~80 experiments measuring total runtime of
--helpcommands:elastic --helpelastic es --helpelastic cloud --helpelastic kb --helpEach commit contains a single performance tweak. Each commit message contains an explanation and perf measurement deltas. Autoresearch also produced quite a bit of ugly code that only improved runtimes by 5-10ms, so I've only kept the most useful improvements.
I highly encourage reading each commit separately rather than the whole diff at once. 🙏
Before/after deltas
Methodology:
hyperfine -w 10 -m 100 $commandHardware:
The following are mean runtimes in milliseconds.
elasticelastic --helpelastic es --helpelastic eselastic cloud --helpelastic cloudelastic kb --helpelastic kb