ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 47

  1. Blog

citty vs commander: 0.022 ms vs 0.011 ms Parse

Aditya Challa·5 October 2026·5 min read

Hands-on
On this page
  1. What I tested
  2. How fast is citty vs commander?
  3. What does each one cost to install and bundle?
  4. Do they behave the same?
  5. Which one should you pick?
  6. Sources
  7. Related

I timed the same CLI define+parse loop on this box: citty 0.2.2 took about 0.022 ms per cold run and commander 15.0.0 about 0.011 ms. That is roughly 2× in commander's favor on parse, while citty's esbuild node minify+gzip landed at 3,644 B against commander's 11,184 B.

Short answer: pick citty when you want a tiny UnJS-style CLI on Node's util.parseArgs with typed args and lazy subcommands. Pick commander when you need the mature option/help ecosystem, variadic arguments, or the fastest recreate+parse on this box. No affiliate links in this post.

What I tested

  • Machine: 8 vCPU Intel Xeon / 15 GB RAM shared Linux cloud box, tested 6 Oct 2026 about 03:40 IST.
  • Versions: citty 0.2.2, commander 15.0.0 (both npm latest that day), Node 20.19.2, esbuild 0.28.2 node ESM minify + gzip -9.
  • Workload: one positional, three flags (--friendly, --count, --verbose). Cold path = redefine the command and parse those argv values 5,000 times per round; warm path for citty = define once and runCommand 5,000 times. Median of 9 rounds, two processes with the library order reversed. Scripts and JSON in /workspace/bench-citty-commander/.
  • Also measured: renderUsage / helpInformation, nested subcommand dispatch, unknown-option errors, boolean --no- negation, extra positionals vs variadic args, install folder size, and engines fields.
  • Not tested: interactive prompts, custom help configuration beyond defaults, commander plugins, Windows argv quirks, Bun/Deno hosts, and Node 22+ even though commander 15 declares engines.node >= 22.12.0 (it still ran here on 20.19.2).

How fast is citty vs commander?

Job (median of 9, ms per define+parse)Run 1Run 2
citty 0.2.2, cold0.02240.0214
commander 15.0.0, cold0.01190.0108
citty, warm (define once)0.02130.0244
citty renderUsage0.040—
commander helpInformation0.023—

Commander won the cold recreate+parse by about 2× on this box. citty's warm path did not pull ahead of its cold path, so most of the cost is inside runCommand / parseArgs rather than defineCommand. Help text generation was in the same ballpark (about 0.02–0.04 ms). For process spawning next to a CLI, see tinyexec vs execa; for colored terminal output, picocolors vs chalk.

What does each one cost to install and bundle?

PackageMinifiedgzipInput filesAlone on disk
citty 0.2.28,977 B3,644 B376 KB (1 package)
commander 15.0.039,145 B11,184 B8252 KB (1 package)

Both are zero-dependency single packages. citty is about 3× smaller gzip and about 3× smaller on disk after a fresh install. That gap matters for published CLIs the same way it did for loggers in consola vs pino and for locking size in CI via size-limit vs bundlesize. If you bundle the CLI with tsup vs unbuild, citty's smaller graph stays small.

Do they behave the same?

Checkcitty 0.2.2commander 15.0.0
Nested subcommand greet Adasub run fired; parent also runsaction fired; returned hi Ada
Unknown option --nopeno throw in this harness (lands in _)commander.unknownOption
Boolean default true + --no-fancyfancy falsesupported via default + --no-
Extra args a b c on one positionalfiles=a, _=[a,b,c]needs a variadic files argument → [a,b,c]
Module formatESMESM (type module)
engines.nodeunset>= 22.12.0 (ran on 20.19.2 here)
Parser baseNode util.parseArgscommander parser

Two practical gaps: commander rejects unknown options by default and has first-class variadic arguments; citty keeps extras on _ and leans on parseArgs. citty's API is object-shaped (defineCommand + args map), which fits typed UnJS tooling; commander is method-chained and has a much larger help/option surface. Path helpers next to argv parsing are covered in pathe vs node:path, and if your CLI globs files, fast-glob vs globby is the sibling choice.

Which one should you pick?

SituationMy pick
New UnJS / Nuxt-style CLI, smallest bundlecitty
Need variadic args, strict unknown-option errors, rich helpcommander
Hot path that rebuilds and parses argv thousands of timescommander (about 2× here)
CommonJS script without a bundlerneither is ideal (both ESM); wrap or dynamic-import
Already deep in commander options/pluginsstay

Bottom line: on this box commander 15.0.0 defined and parsed in about 0.011 ms versus citty 0.2.2 at about 0.022 ms, while citty shipped at 3.6 KB gzip against 11.2 KB. Who should not switch to citty: teams that depend on commander's option validation, variadic args, or plugin ecosystem. Who should not stay on commander for a greenfield tiny CLI: anyone optimizing install and bundle size who is fine with parseArgs-shaped flags.

How this was made: I installed both packages on the ShopperCove box, timed cold define+parse (and citty warm) over 5,000 iterations × 9 rounds in two order-reversed processes, bundled each with esbuild, and checked nested commands, unknown options, negation, and variadic behavior. The JSON results sit next to the scripts. The write-up was drafted with AI help and checked against that output.

Sources

  • https://github.com/unjs/citty
  • https://github.com/tj/commander.js
  • https://nodejs.org/api/util.html#utilparseargsconfig
  • https://www.npmjs.com/package/citty
  • https://www.npmjs.com/package/commander
  • https://esbuild.github.io/

Related

  • https://www.shoppercove.com/blog/tinyexec-vs-execa
  • https://www.shoppercove.com/blog/consola-vs-pino
  • https://www.shoppercove.com/blog/picocolors-vs-chalk
  • https://www.shoppercove.com/blog/size-limit-vs-bundlesize
  • https://www.shoppercove.com/blog/tsup-vs-unbuild
  • https://www.shoppercove.com/blog/pathe-vs-node-path
  • https://www.shoppercove.com/blog/fast-glob-vs-globby
  • https://www.shoppercove.com/blog/knip-vs-depcheck
cittycommanderclinodejsperformanceparseargsbundle size

Lab evidence

What I found running this

Hands-on on ShopperCove box 6 Oct 2026 ~03:40 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 20.19.2). citty 0.2.2 vs commander 15.0.0; esbuild 0.28.2 node ESM minify + gzip -9. Workload: positional name + --friendly/--count/--verbose. Cold = redefine + parse 5,000× per round, median of 9, order-reversed (ms): citty 0.0224/0.0214, commander 0.0119/0.0108. citty warm (define once) 0.0213/0.0244. Usage/help ~0.040 vs ~0.023 ms. Bundle 8,977/3,644 B vs 39,145/11,184 B. Install 76 KB vs 252 KB. Behavior: both dispatch nested greet; commander throws unknownOption; citty --no-fancy clears default-true boolean; citty extra positionals land on _; commander needs variadic. commander package engines >=22.12.0, still ran here. Not tested: prompts, plugins, Windows, Bun/Deno, Node 22+. No affiliate.

Notes when a lab post goes up

Occasional email for new hands-on reviews. No sequence and no sponsors.

Related links

  • Plate 91

    Tinyexec vs execa: 2.7 KB vs 37 KB Gzip

    5 Oct 2026

  • Plate 91

    tailwind-merge vs clsx: 8.6 KB vs 264 B

    5 Oct 2026

  • Plate 41

    ofetch vs ky: 4.1 KB vs 9.8 KB Gzip

    5 Oct 2026

On this page

  1. What I tested
  2. How fast is citty vs commander?
  3. What does each one cost to install and bundle?
  4. Do they behave the same?
  5. Which one should you pick?
  6. Sources
  7. Related
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove