c8 12.0.0 vs nyc 18.0.0 on 80 CommonJS modules / 480 mocha tests: coverage-run median 4222 ms vs 7415 ms (~1.76x; no-coverage baseline 1734 ms). Same code, different totals: branches 81.3% (c8) vs 47.9% (nyc).
5 min read
On 80 CommonJS modules with 480 mocha tests, c8 12.0 finished a full coverage run in a median 4.2 s and nyc 18.0 took 7.4 s (~1.76x slower), against 1.7 s with no coverage at all. The two tools also disagreed on the numbers: c8 reported 81.3% branches and 63.3% lines, nyc reported 47.9% branches and 58.5% lines for the same run.
Short answer: switch to c8 if your tests run on plain Node and you want lower coverage overhead with no instrumentation step. Stay on nyc if you rely on Babel-instrumented code, Istanbul-specific ignore hints, or coverage thresholds you cannot re-baseline this sprint. No affiliate links in this post.
src/ (7,280 lines, 8 functions each with if/else-if/else loops, plus one never-called function per file) and 80 mocha test files (480 tests). Generator script kept with the raw results.c8 12.0.0, nyc 18.0.0, mocha 12.0.3. Same config for both: include every .js file under src/, all: true, reporters text-summary and lcov.mocha, c8 mocha, nyc mocha (default cache on), nyc --cache=false mocha. Coverage folders deleted before every run. Wall clock via process.hrtime around the full npx command.--experimental-monocart flag, Windows/macOS, suites above a few thousand tests.| Variant | Median wall | Runs (ms) | Overhead vs no coverage |
|---|---|---|---|
| mocha, no coverage | 1734 ms | 1734 / 1665 / 1665 / 1816 / 1759 | none |
| c8 12.0 | 4222 ms | 4359 / 4177 / 4135 / 4222 / 4340 | +2488 ms (2.4x) |
| nyc 18.0, cache on | 7415 ms | 7997 / 7415 / 8167 / 7323 / 7386 | +5681 ms (4.3x) |
| nyc 18.0, cache off | 8678 ms | 8678 / 9065 / 8562 / 8766 / 8452 | +6944 ms (5.0x) |
Yes, on this fixture. c8 reads coverage that V8 already collects while the code runs, so there is no source rewrite before tests start. nyc rewrites every included file with Istanbul counters first, and the counters also slow down hot loops like the ones in this fixture. Its cache only removed about 1.3 s of the cost.
Test-runner choice is a separate decision. The Jest vs Vitest post and the Vitest 5 upgrade post cover the runner itself; this post only covers coverage for a mocha or plain Node suite. If you also need a DOM, the jsdom vs happy-dom post has those numbers.
| Metric | c8 12.0 | nyc 18.0 |
|---|---|---|
| Statements | 63.31% (4609/7280) | 57.97% (5009/8640) |
| Branches | 81.26% (2642/3251) | 47.89% (2529/5280) |
| Functions | 66.66% (480/720) | 66.66% (480/720) |
| Lines | 63.31% (4609/7280) | 58.50% (3089/5280) |
c8 converts V8 byte ranges to lines, so every physical line counts, including closing braces. nyc counts the statements and branch paths Istanbul inserted at instrumentation time. Function coverage matched exactly at 480 of 720. Branch coverage moved the most, by 33 points. Plan to reset your thresholds the day you switch, and compare per-file reports before trusting the new totals.
Disk use also differed. c8 left 3.9 MB in coverage/ (the lcov report plus raw V8 JSON in coverage/tmp). nyc left 1.6 MB in coverage/ plus 2.8 MB in .nyc_output/. Both lcov.info files were about 150 to 160 KB.
| Situation | My pick |
|---|---|
| Plain Node or mocha suite, CI time matters | Switch to c8; re-measure on your suite |
| Coverage gate in CI tuned to nyc numbers | Switch only with a planned threshold reset |
Code is instrumented through Babel (babel-plugin-istanbul) | Stay on nyc |
You depend on /* istanbul ignore */ hints everywhere | Test c8 first; it supports c8 ignore hints instead |
| Tests already run in Vitest or Jest | Use that runner's coverage provider, not either CLI |
Bottom line: on 480 mocha tests, c8 added about 2.5 s of coverage cost and nyc added about 5.7 s, so c8 was roughly 1.76x faster end to end. Who should not switch yet: teams whose release gate depends on today's nyc percentages and who cannot re-baseline. For a Node runtime choice on the same box, see the tsx vs ts-node post.
How this was made: I generated the fixture, ran the interleaved coverage runs on the ShopperCove test box, and kept the timing JSON; the write-up was drafted with AI help and checked against that output.
Lab evidence
Hands-on on ShopperCove box 5 Oct 2026 ~23:13-23:15 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 20.19.2). Fixture: 80 CJS modules in src/ (7,280 lines, 8 branchy functions + 1 unused each), 80 mocha test files / 480 tests. Versions: c8 12.0.0, nyc 18.0.0, mocha 12.0.3; same include src/*.js, all:true, reporters text-summary+lcov. 1 nyc cache warmup, then 5 interleaved rounds, coverage dirs deleted each run. Medians: no coverage 1733.9 ms (1733.9/1664.6/1664.8/1816/1758.9); c8 4221.8 ms (4359.2/4176.7/4135.4/4221.8/4340.1); nyc cache-on 7414.5 ms (7997/7414.5/8167.1/7323.3/7385.9); nyc --cache=false 8677.9 ms (8677.9/9065/8562/8765.5/8451.5). Coverage: c8 stmts 63.31% (4609/7280), branches 81.26% (2642/3251), funcs 66.66%, lines 63.31%; nyc stmts 57.97% (5009/8640), branches 47.89% (2529/5280), funcs 66.66%, lines 58.5% (3089/5280). Disk: c8 coverage/ 3.9 MB (incl. tmp V8 JSON); nyc coverage/ 1.6 MB + .nyc_output 2.8 MB. Raw: /workspace/bench-c8-nyc/res-c8-nyc.json. Not tested: TS/ESM sources, Babel instrumentation, Jest/Vitest providers, --experimental-monocart, Windows/macOS. No affiliate.
npm 12.2.0 vs pnpm 12.9.1 vs Bun 1.4.2 on one 24-dependency React + Vite app: from scratch 14.2 s / 3.1 s / 1.5 s; lockfile + warm cache 2.9 s / 0.28 s / 0.27 s; lockfile + empty cache 3.9 s / 2.3 s / 0.45 s. Disk, lockfile size and the pnpm phantom-dependency catch.
Summary5 Oct 20267 min read
Hands-on6 Oct 20269 min read
Hands-on6 Oct 20268 min read