↖ Home

node --run is fast

Published | Last updated

Developers 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 test script of the package.json in the current folder:

$ node --run test

You 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:

CommandRelativeMean [ms]
node --run testBaseline9.5 ± 1.7
npm run test~8.3x slower78.4 ± 2.2
npm test~8.4x slower79.6 ± 3.7
yarn test~14.2x slower135.1 ± 3.6
yarn run test~14.5x slower137.5 ± 5.6
pnpm test~23x slower218.9 ± 17.1
pnpm run test~21.5x slower204.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:

CommandMean [ms]Min [ms]Max [ms]Relative
node --run test9.5 ± 1.75.711.91.00
npm test79.6 ± 3.775.191.58.38 ± 1.53
npm run test78.4 ± 2.275.182.68.25 ± 1.48
yarn test135.1 ± 3.6130.2145.014.23 ± 2.54
yarn run test137.5 ± 5.6130.4152.814.48 ± 2.63
pnpm test218.9 ± 17.1196.6245.723.05 ± 4.45
pnpm run test204.1 ± 5.8198.9222.121.49 ± 3.85

Caveats

  1. Flag handling is strict: any flags before -- are passed to node. (Addressed in the section below.)
  2. Unlike npm run, node --run without arguments does not list all available scripts. (Addressed in the section below.)
  3. node --run does not run pre and post scripts. Workflows relying on pre/post scripts won’t be compatible with node --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