Node Module Compile Cache: Faster Cold Starts for Heavy CLIs

Node Module Compile Cache: Faster Cold Starts for Heavy CLIs

Large TypeScript-authored CLIs spend a humiliating amount of time parsing and compiling JS on every cold invoke. Node’s module compile cache (and related bytecode caching) stores the results on disk so the second — and every teammate’s first after a shared cache — starts closer to “already warmed.” Used poorly, you cache the wrong tree and ship stale bytecode; used well, you shave hundreds of milliseconds off DX and Lambda-style warmers.

⚡ TL;DR: Enable compile cache via NODE_COMPILE_CACHE (or version-appropriate module compile cache flags) pointing at a writable, per-version directory; key the cache on Node version + lockfile hash; never share caches across major Node upgrades; measure process.uptime() to first productive output. Related: AWS Lambda Cold Start Fix and Lambda Warm Pools.

Turn it on with a deterministic cache root

# ✅ Per-Node, per-project cache path
export NODE_COMPILE_CACHE="$HOME/.cache/node-compile/$(node -v)/acme-cli-$(sha256sum pnpm-lock.yaml | cut -c1-12)"
mkdir -p "$NODE_COMPILE_CACHE"
node ./dist/cli.js --help
// src/cli-entry.ts
if (!process.env.NODE_COMPILE_CACHE) {
  // Soft warn in dev — hard fail in perf CI
  console.warn("NODE_COMPILE_CACHE unset; cold starts will parse everything");
}
# ❌ Single global cache across Node 20 and 22 and every repo
export NODE_COMPILE_CACHE=/tmp/node-cache

Measure the only metric that matters

Time from process start to first actionable output (help text, resolved config, or first network call), not just require count.

// src/boot-metrics.ts
const t0 = performance.now();

export function markReady(label: string) {
  const ms = performance.now() - t0;
  if (process.env.CLI_BOOT_METRICS) {
    console.error(JSON.stringify({ event: "cli_ready", label, ms }));
  }
}

// after config load
markReady("config_loaded");

CI: warm the cache once, then assert

# .github/workflows/cli-boot.yml
- name: Warm compile cache
  run: |
    export NODE_COMPILE_CACHE=$RUNNER_TEMP/cc
    mkdir -p "$NODE_COMPILE_CACHE"
    node dist/cli.js --help >/dev/null
    CLI_BOOT_METRICS=1 node dist/cli.js --help
- name: Cold-ish second process should be faster
  run: |
    export NODE_COMPILE_CACHE=$RUNNER_TEMP/cc
    CLI_BOOT_METRICS=1 node dist/cli.js --help | tee boot.json
    # assert ms under budget with jq in a real job

Interaction with bundlers and ESM

If you already bundle the CLI into a single file with esbuild/ncj, compile cache helps less — the bottleneck moved to your bundle strategy. Prefer: tree-shake heavy deps, lazy-import() subcommands, then compile cache for whatever remains as plain node_modules during local ts-node/tsx paths.

// ✅ Lazy subcommands — biggest win before caching
export async function run(argv: string[]) {
  if (argv[0] === "deploy") {
    const { deploy } = await import("./cmds/deploy.js");
    return deploy(argv.slice(1));
  }
  if (argv[0] === "sync") {
    const { sync } = await import("./cmds/sync.js");
    return sync(argv.slice(1));
  }
}

Lambda and container notes

Ephemeral filesystems make caches less sticky unless you attach a volume or rely on provisioned concurrency warmers. For Lambda, prefer smaller bundles + provisioned concurrency over compile cache on /tmp unless you control reuse patterns carefully.

Closing checklist

✅ Dos
– ✅ Set NODE_COMPILE_CACHE to a version+lockfile keyed directory
– ✅ Lazy-load CLI subcommands before tuning caches
– ✅ Record time-to-ready in CI budgets
– ✅ Wipe caches on Node major upgrades
– ✅ Combine with bundling for distributed binaries

❌ Don’ts
– ❌ Don’t share one cache across Node majors
– ❌ Don’t commit cache directories to git
– ❌ Don’t expect compile cache to fix multi-hundred-MB dependency graphs alone
– ❌ Don’t enable experimental flags in prod without a rollback env knob
– ❌ Don’t confuse compile cache with V8 code cache inside bundled singles

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply