Benchmarking Bun's cold start against Node with performance.now()
Published April 30, 2027
const measureColdStart = (cmd: string[]) => {
const start = performance.now();
Bun.spawnSync(cmd);
return performance.now() - start;
};
const runs = 5;
const bunTimes = Array.from({ length: runs }, () => measureColdStart(["bun", "-e", ""]));
const nodeTimes = Array.from({ length: runs }, () => measureColdStart(["node", "-e", ""]));
const avg = (times: number[]) => times.reduce((a, b) => a + b, 0) / times.length;
console.log(`bun: ${avg(bunTimes).toFixed(1)}ms`);
console.log(`node: ${avg(nodeTimes).toFixed(1)}ms`);What
Wrapping Bun.spawnSync(["bun"/"node", "-e", ""]) in performance.now() calls times how long
each runtime takes to start a process, run an empty script, and exit — a direct measurement of
cold-start overhead rather than a claim from a README.
Why it matters
Startup time is one of the more commonly repeated Bun-vs-Node numbers, but a claim from a blog
post depends entirely on that author's machine, OS, and Bun/Node versions — worth measuring on
whatever machine actually matters to you (a laptop, a specific CI runner, a container base
image) rather than trusting a number measured somewhere else. performance.now() gives
sub-millisecond, monotonic timestamps unaffected by wall-clock adjustments, and wrapping a
subprocess spawn in it measures exactly the thing "cold start" usually means: time from process
creation to a script finishing and exiting, for a runtime with nothing already warmed up.
How it works
measureColdStartspawns the given command synchronously —Bun.spawnSyncblocks until the child exits, which is exactly what a cold-start measurement wants: no overlap with a previous run's timing.-e ""runs an empty script in both runtimes — the timing captures pure startup/teardown overhead, not any work the script does.- Running
runsiterations per runtime and averaging smooths out one-off noise (OS scheduling, disk cache state on the very first run) that a single measurement wouldn't. avgis a plain reduce over the collected times — nothing benchmark-framework-specific, since the interesting part here is the spawn+timing pattern, not statistical rigor.
Gotchas
- This measures wall-clock time in the parent process around a blocking spawn, not anything reported by the child — it necessarily includes OS process-creation overhead alongside runtime startup, which is usually what "cold start" means in practice anyway.
- The first run of a loop is often slower than the rest (disk cache, OS scheduler warm-up) — either discard the first result or run enough iterations that it doesn't skew the average much.
- This requires both
bunandnodeto be installed and resolvable onPATH— there's no way to benchmark a runtime that isn't actually present to spawn.
Related
Bun.spawn(the async version) is the better choice for benchmarking something that overlaps with other work;spawnSynchere is deliberate, since overlap is exactly what would corrupt a cold-start measurement.proc.resourceUsage()after a spawn reports CPU time and memory separately from wall-clock time — useful alongside this if the question is resource cost rather than latency.