Benchmarks

Task runners spend most of their wall-clock on the work they launch. To compare the runners themselves, these scenarios run tasks that do almost nothing and measure what's left: process startup, config parsing, and the cost of reaching the child commands.

The same task definitions are generated for every tool (tsr, npm, bun, deno, just, go-task, make, and mise) — see benches/ — and timed with hyperfine. The numbers below are loaded directly from the benchmark's JSON export.

startup — a single task

Spawns true. This is pure per-invocation overhead, and where the compiled runners separate from the interpreter-based ones.

make
1.6 ms · 1.0×
tsr
1.8 ms · 1.2×
just
2.4 ms · 1.5×
bun
3.1 ms · 1.9×
deno
7.8 ms · 4.8×
mise
22.1 ms · 13.8×
npm
93.2 ms · 58.3×
task
107.3 ms · 67.1×

Mean wall-clock, lower is faster. × is relative to the fastest runner (make, 1.6 ms).

shell one-liner — $VAR + &&

A task defined as echo $HOME && echo done. This is where tsr's mini-shell earns its place: it expands $VAR and sequences with &&/||/; in-process, without launching a shell. tsr spawns each command directly, so on a one-liner that a real shell would run with builtins it can be a touch slower — the win comes when the commands are real programs. (Pipes and redirects aren't in the mini-shell; those use a delegate to sh -c as an escape hatch, not measured here.)

make
2.0 ms · 1.0×
just
2.4 ms · 1.2×
tsr
2.8 ms · 1.4×
bun
3.1 ms · 1.5×
deno
7.4 ms · 3.7×
mise
22.7 ms · 11.3×
npm
96.1 ms · 48.0×
task
109.0 ms · 54.4×

Mean wall-clock, lower is faster. × is relative to the fastest runner (make, 2.0 ms).

local binary — resolving node_modules/.bin

The real "npm run replacement" case: a task that calls a project-local tool like vite or eslint. These are Node programs, so every runner ultimately spawns Node; what differs is the runner's own startup on top. Only the runners that resolve node_modules/.bin can do this — tsr, npm, bun, and deno — so this is a four-way comparison (just/make/go-task/mise don't resolve project-local binaries, so a bare recipe wouldn't find the tool at all). tsr prepends node_modules/.bin to PATH like npm/bun/deno, then spawns the tool directly — skipping npm's extra Node startup.

bun
22.4 ms · 1.0×
tsr
27.9 ms · 1.2×
deno
32.8 ms · 1.5×
npm
113.1 ms · 5.1×

Mean wall-clock, lower is faster. × is relative to the fastest runner (bun, 22.4 ms).

in-task steps — one launch, five commands

One task that runs five commands in sequence. Every runner launches once and then spawns the child commands, so even npm/bun stay cheap here — the startup tax is paid a single time.

bun
3.0 ms · 1.0×
make
3.5 ms · 1.2×
just
4.7 ms · 1.6×
tsr
5.5 ms · 1.8×
deno
7.5 ms · 2.5×
mise
28.4 ms · 9.6×
npm
94.0 ms · 31.7×
task
114.1 ms · 38.4×

Mean wall-clock, lower is faster. × is relative to the fastest runner (bun, 3.0 ms).

dependency graph — five tasks

One task depending on five trivial tasks. tsr, just, go-task, make, and mise resolve the whole graph in a single launch. npm/bun have no dependency graph, so — as their users do — the five tasks are chained with &&, paying the runner's startup five times over.

make
3.7 ms · 1.0×
just
4.7 ms · 1.3×
tsr
5.6 ms · 1.5×
bun
14.2 ms · 3.8×
deno
35.0 ms · 9.4×
mise
37.8 ms · 10.2×
task
113.8 ms · 30.7×
npm
458.8 ms · 123.7×

Mean wall-clock, lower is faster. × is relative to the fastest runner (make, 3.7 ms).

dependency graph — ten tasks

The same, scaled to ten dependencies. Overhead that is paid per-invocation grows linearly: the graph-native runners barely move, while the chained npm/bun costs roughly double from the five-task graph.

make
5.9 ms · 1.0×
just
7.2 ms · 1.2×
tsr
10.4 ms · 1.7×
bun
28.3 ms · 4.8×
mise
58.2 ms · 9.8×
deno
68.9 ms · 11.6×
task
119.5 ms · 20.2×
npm
910.4 ms · 153.6×

Mean wall-clock, lower is faster. × is relative to the fastest runner (make, 5.9 ms).

Takeaway

For a single task, tsr runs a metacharacter-free run string by spawning the child directly (execvp-style) — no language runtime, no wrapping shell — so it sits with the native runners (make, just) and well ahead of npm run. The gap compounds across a dependency graph: because tsr resolves the entire graph in one process, its cost stays flat while a chained npm/bun pays its startup once per task. That is the case tsr is built for.

Not a build-performance claim

This measures runner overhead only. Content-hash and incremental caching are deliberately delegated to Turbo/Nx (see the overview) — out of scope here.

Your numbers will differ

Measured on one reference machine (Linux x86-64). Regenerate with benches/run.sh, then node website/tools/sync-bench.mjs to refresh this page.