~1-1.6s of fixed JS startup on every Hardhat invocation
A no-op hardhat compile costs 1.0–1.6 s of wall-clock. Measured in the Hardhat 3 + EDR profiling campaign, self-time splits 58% node/V8, 28% JIT'd JS, 8% libc; categorised on aave-v4's warm compile: JIT compile 22%, fs I/O 19%, GC 19%, module loader 16%, JS parse 9%, JSON parse 3%. Two thirds of the run is loading, parsing, compiling and GC'ing the CLI's module graph before any work begins.
The --cpu-prof pass names the pieces (shares of active startup JS): wrapSafe (CJS compile) 6–7%, compileSourceTextModule (ESM compile) ~5%, internalModuleStat/readdir/readFileUtf8 (resolution stat storm) 6–8% combined, source_map_cache.lineLengths ~2% (source-map registration during startup — worth checking why it is on), semver's top-level evaluation ~1%. Node's module machinery totals ~48% of active startup JS ≈ ~25% of warm-compile wall.
Every invocation pays it: ~30–60% of warm-compile wall, ~10–40% of a min-deps incremental compile, and it dominates short test suites — 1inch-aqua's test run spends 67% of its CPU in node.
Direction
Three independent avenues, in expected-impact order: (a) enable Node's on-disk compile cache (module.enableCompileCache(), Node ≥22.8) at CLI entry, which attacks the JIT-compile plus parse share directly; (b) audit eager imports at startup against the repo's own lazy-import rules, and check whether source-map support must be enabled that early; (c) revisit build-info/artifact JSON reads on the no-op path, which should not need parsing to decide there is nothing to do.
Implementation plan
# Cut Hardhat's fixed CLI startup cost (~1–1.6 s per invocation)
## Problem
[Profiling Hardhat 3 e2e scenarios](https://app.notion.com/p/nomicfoundation/Runtime-Profiling-2026-08-05-3b3578cdeaf5808eafdff4e22ba425d0?source=copy_link) measures a no-op `hardhat compile` at 1.0–1.6 s of wall-clock per scenario. Self-time splits 58% node/V8, 28% JIT'd JS, 8% libc; categorised on aave-v4's warm compile: JIT compile 22%, fs I/O 19%, GC 19%, module loader 16%, JS parse 9%, JSON parse 3%. Two thirds of it is loading, parsing, compiling and GC'ing the CLI's module graph before any work starts.
The `--cpu-prof` pass names the pieces (shares of active startup JS): `wrapSafe` (CJS compile) 6–7%, `compileSourceTextModule` (ESM compile) ~5%, `internalModuleStat`/`readdir`/`readFileUtf8` (resolution stat storm) 6–8% combined, `source_map_cache.lineLengths` ~2%, `semver`'s top-level evaluation ~1%. Node's module machinery is ~48% of active startup JS ≈ ~25% of warm-compile wall.
Every invocation pays this. It is ~30–60% of warm-compile wall, ~10–40% of a min-deps incremental compile, and it dominates small test suites (1inch-aqua's test run spends 67% of CPU in node).
## Task
Three independent avenues, in expected-impact order. Land them as separate commits.
1. **Node compile cache.** Node ≥22.8 ships `module.enableCompileCache()` (transparent on-disk V8 code cache). Call it as early as possible in the CLI entry (`packages/hardhat/src/internal/cli/main.ts` or the bin shim; consider `hardhat/dist` consumers too). This attacks the 22% JIT-compile + 9% parse share on second and later runs. Verify the cache location behaviour, and that it is safe with pnpm's store layout and with Hardhat's own re-exec and subprocess spawns (mocha workers, solc runs).
2. **Eager-import audit.** Use a `--cpu-prof` capture of a warm compile to find heavyweight modules loaded before command dispatch that violate the repo's lazy-import rules (CLAUDE.md conditions 1–6), and confirm each fix with a fresh capture. Check why source-map registration is active at startup (`--enable-source-maps` or `process.setSourceMapsEnabled`) — deferring or disabling it is a candidate win. Reference lazy patterns: `packages/hardhat-ignition-ethers/src/internal/hook-handlers/network.ts`, `docs/engineering-guidelines.md` GC3.
3. **Startup JSON/artifact I/O.** A no-op compile spends 19% fs I/O + 3.4% JSON parse — identify what is read (build info? artifacts? cache manifests?) and whether it can be deferred, memoised or slimmed (e.g. deciding "nothing to do" without parsing full build info).
Follow repo rules (`HardhatError` only, lazy-import justification comments where required) and run `pnpm lint`, `pnpm build`, `pnpm test` in `packages/hardhat`.
## Verification (before/after)
Profile before and after with `pnpm profiler` (a bare `pnpm profiler` prints its usage; see `scripts/README.md`):
```bash
pnpm build
pnpm profiler --scenario ./end-to-end/uniswap-x \
--prepare "cold compile" --command "warm compile" \
--mode js --init --use-local
```
Primary metric: warm-compile `wallSeconds` in `status.json`, target −0.5 s or better. Secondary: `hyperfine -w 2 'npx hardhat compile'` in the scenario clone. Node's module-loading internals should shrink in the `.cpuprofile`, and behaviour must be unchanged (`npx hardhat test solidity` still passes).Source: NomicFoundation/hardhat