node --run is fast
Published | Last updatedDevelopers in the Node.js and web ecosystems run scripts defined in package.json dozens of times daily, so even a miniscule increase in performance could improve developer experience. I found that Node’s --run flag launches scripts 8 times faster than npm run, yielding a noticeable reduction in startup overhead.
Introduction to node --run
Node.js v22.0.0 introduced the --run flag as an alternative to npm run, yarn run, etc.:
…the following command will run the
testscript of thepackage.jsonin the current folder:$ node --run testYou can also pass arguments to the command. Any argument after
--will be appended to the script:$ node --run test -- --verbose
Benchmark
I ran a benchmark with a no-op script to highlight the differences in overhead between the contenders:
| Command | Relative | Mean [ms] |
|---|---|---|
node --run test | Baseline | 9.5 ± 1.7 |
npm run test | ~8.3x slower | 78.4 ± 2.2 |
npm test | ~8.4x slower | 79.6 ± 3.7 |
yarn test | ~14.2x slower | 135.1 ± 3.6 |
yarn run test | ~14.5x slower | 137.5 ± 5.6 |
pnpm test | ~23x slower | 218.9 ± 17.1 |
pnpm run test | ~21.5x slower | 204.1 ± 5.8 |
With real-world scripts, the differences would be less noticeable—this benchmark focuses solely on differences in script startup time.
Methodology
I created a package.json file with the following contents
{ "scripts": { "test": "true" } }
This defines a single script named test that runs the shell built-in command true, which executes almost instantly and does nothing.
I then used the following hyperfine command to create the benchmark:
hyperfine --warmup=1 --shell=none --reference \
'node --run test'\
'npm test' \
'npm run test' \
'yarn test' \
'yarn run test' \
'pnpm test' \
'pnpm run test'
Node.js version 24.7.0, hyperfine version 1.19.0. --warmup because pnpm ran slower on its first run, which would skew the results without this flag.
Full benchmark output:
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
node --run test | 9.5 ± 1.7 | 5.7 | 11.9 | 1.00 |
npm test | 79.6 ± 3.7 | 75.1 | 91.5 | 8.38 ± 1.53 |
npm run test | 78.4 ± 2.2 | 75.1 | 82.6 | 8.25 ± 1.48 |
yarn test | 135.1 ± 3.6 | 130.2 | 145.0 | 14.23 ± 2.54 |
yarn run test | 137.5 ± 5.6 | 130.4 | 152.8 | 14.48 ± 2.63 |
pnpm test | 218.9 ± 17.1 | 196.6 | 245.7 | 23.05 ± 4.45 |
pnpm run test | 204.1 ± 5.8 | 198.9 | 222.1 | 21.49 ± 3.85 |
Caveats
- Flag handling is strict: any flags before
--are passed tonode. (Addressed in the section below.) - Unlike
npm run,node --runwithout arguments does not list all available scripts. (Addressed in the section below.) node --rundoes not runpreandpostscripts. Workflows relying onpre/postscripts won’t be compatible withnode --run.
Convenience wrapper
To overcome caveats #1 and #2, I use the following shell function:
nr() {
node --run "${1-listing all scripts}" -- "${@:2}"
}
The first argument, $1, is the name of the script. Subsequent arguments, ${@:2}, are interpreted as flags for the script. This allows for passing arguments to the script without the -- separator:
$ node --run test -- --verbose
$ nr test --verbose
When no arguments are provided, ${1-listing all scripts} triggers the script listing:
$ node --run "<intentionally invalid script name>"
$ nr
Missing script: "listing all scripts" for ~/src/node-run-test/package.json
Available scripts are:
test: true