192. How fast is the compiler?
Every benchmark so far timed the programs Viscosity writes. This one times Viscosity. The Rust CLI is about ten times faster than the TypeScript one, the engine alone about five, and the gap between those two numbers is mostly the time it takes to start a program at all.
- Tell whole-process compile times from in-process ones, and say which question each answers.
- Read section (b) of the benchmark matrix, and explain where a CLI run's time goes.
- Explain why a ratio of small times is fragile, and why the matrix sums medians.
- Predict how start-up, per-file cost and threads matter as the number of files grows.
The other stopwatch
Since Part II, every benchmark in this book has timed the programs
Viscosity writes: does the struct-of-arrays version of particles
run faster than the original? This chapter turns the stopwatch on
Viscosity itself. When you type viscosity build app.js, how long
until you get your prompt back? And when the playground recompiles on
every keystroke, how long does each compile take?
Those are two different questions, and they get two different answers.
How fast is a taxi? The speed on the motorway is one answer. The time from phoning for it to arriving is another, and for a short trip it is mostly the wait for the car to show up. A faster engine doesn’t help much if the car is parked across town.
Compile-time performance is the speed of the compiler itself, from
source in to code out. Measure it as a whole process per compile
(what a command-line user waits for, start-up included) or in
process: compile() called again and again in one long-lived
process, after warm-up (what an editor, a build tool or a playground
sees). Neither says anything about how fast the compiled program runs.
Section (b) of the matrix
engine/ts/packages/bench/src/matrix.ts measured both, on the 40
JavaScript programs of the corpus, and wrote the results to
results/matrix-2026-10-31.json. Whole process: viscosity build <file>, five runs per program, alternating the TypeScript CLI (Node,
running the esbuild bundle from chapter 119) and the Rust CLI (the
release binary). In process: 50 warm-up calls, then 200 timed calls,
of the TypeScript engine and of @viscosity/wasm in the same Node. The
outputs were checked to be identical. Every number below comes from
that file:
Where the time goes
The third step is the important one. A warm compile() is under 1% of
a TypeScript CLI run, for every program. So what is the other 99%? I
timed the pieces on the same shared VM, 21 fresh processes each,
alternating, in four rounds. The load average was between 2 and 15
(other jobs were building), so I give each median as a range across
the rounds:
node -e 0 26–40 ms
TS CLI --version 67–91 ms
TS CLI build (1 file) 83–120 ms
Rust CLI --version 2.6–3.6 ms
Rust CLI build 3.2–4.3 ms
Starting Node is about a third of the TypeScript run. Loading the 1 MB
bundle costs more than that again, before a line is compiled:
--version compiles nothing. The compile itself adds 15 to 30 ms, far more than
the warm figure, because in a fresh process the code is cold. The
committed engine/rust/results/compile-speed-2026-10-31.md measured
this directly: the first compile() of boids took 22 ms, about 25
times its warm time. That is chapter 24’s JIT warm-up, seen from the
compiler’s side.
The Rust CLI spends almost all of its 3 ms starting a process too. That’s why the CLI ratio is twice the engine ratio: it mostly compares Node’s start-up with a native binary’s.
Notice that my Rust CLI medians (3 to 4 ms) are well below the
matrix’s (7 to 15 ms), for the same binary. The matrix ran while the VM
was busy, and its file records load averages near 4. When the
denominator is a few milliseconds, a busy neighbour can double it, and
the ratio halves. Read “10.9×” as “about ten times, or more”. The
earlier compile-speed file shows the other way a ratio moves. It ran
the TypeScript CLI from source, node cli.ts, which Node type-strips
and loads module by module, and found medians near 200 ms and gaps of
80 to 120 times. The bundle halved the TypeScript start-up.
Many files, one process
Both CLIs accept many files: viscosity build a.js b.js … -o out/.
Then start-up is paid once. I timed 1, 40 and 400 files in one
process (the corpus, then the corpus copied ten times), with
--jobs 1, writing to tmpfs, seven runs each, in two rounds. The two
CLIs’ outputs for the corpus were identical. Medians: the Rust CLI
took 3.6–3.7, 7.5–8.4 and 37 ms, the TypeScript CLI 86–92, 144–156 and
508–667 ms. So each extra file cost Rust under 0.1 ms and TypeScript
1 to 1.5 ms, more than its warm in-process time. (That loop repeats one program 250 times; this one
reads and writes 400 different files. I didn’t dig further.) The model
below is those two costs, rounded. Watch the ratio fall from about 25×
towards the ratio of the per-file costs:
// A two-number model of a CLI run:
// start-up, plus a cost per file.
// Rounded from this chapter's runs
// of 1, 40 and 400 files (shared VM).
struct Cli {
start_ms: f64,
per_file_ms: f64,
}
impl Cli {
fn run(&self, files: f64) -> f64 {
self.start_ms
+ files * self.per_file_ms
}
}
fn main() {
let ts = Cli {
start_ms: 90.0,
per_file_ms: 1.2,
};
let rs = Cli {
start_ms: 3.5,
per_file_ms: 0.08,
};
for n in [1.0, 10.0, 100.0, 1e4] {
let a = ts.run(n);
let b = rs.run(n);
println!(
"{n:>6} files: {a:>8.1} ms \
vs {b:>6.1} ms = {:.0}x",
a / b
);
}
}
With hundreds of files, the per-file cost dominates, and threads can
share it out. results/parallel-build-2026-10-31.md built 396 files
with one to four threads. Writing to tmpfs, four threads were 2.95×
faster than one. Writing to the VM’s disk, only 1.24×, because creating
396 files took about 65 ms whatever the thread count. Chapter 195 is
about those threads.
What does the native engine do with its 0.1 ms? The compile-speed file
reports a callgrind profile of nbody.js. Parsing with oxc took about
10–17 µs, scope resolution plus lowering about 80, analysis about 35,
plan and emit about 25. No phase dominates. The largest costs are
small heap allocations (strings in the IR, per-block maps) and hashing,
and switching the internal maps from SipHash to FxHash cut 10–40%. The
port stopped there on purpose: it is a faithful translation of the
reference, not a rewrite for speed. The matrix’s in-process Rust
column is the WebAssembly build, not this native one, and it has one
caveat. Its timed calls free each result without
reading code and explain, which skips copying the strings out of
WebAssembly memory. I timed both ways: reading them added about 3%.
Your turn
The matrix summarizes many noisy timings in two steps. Write median: sort a copy (use total_cmp for f64), take the middle value, or the mean of the two middle values for an even count. Then write total_speedup: the sum of the TS programs’ medians divided by the sum of the Rust programs’ medians. (Not the mean of per-program ratios: a slow program should count for more.)
// The median of a sample, the way
// matrix.ts computes it.
fn median(xs: &[f64]) -> f64 {
todo!()
}
// ts[p], rs[p]: the timings of program
// p. The matrix's total: the sum of the
// medians, TS over Rust.
fn total_speedup(
ts: &[Vec<f64>],
rs: &[Vec<f64>],
) -> f64 {
todo!()
}What you learned
- Compile-time performance is the compiler’s own speed: per process for a command line, in process for editors and playgrounds.
- Over the corpus the Rust CLI was about ten times faster, the engine alone (as WebAssembly) about five.
- A CLI run is mostly start-up: Node and its bundle, against a few milliseconds for a native binary. The first compile in a process is also cold.
- Ratios of milliseconds are fragile on a shared machine. Sum the medians and report the load.
- With many files, start-up amortizes. Per-file cost and threads decide, and the disk can cap them.
Check your understanding
0/6- 1.
A user runs
viscosity build app.jsonce from a terminal. Which number predicts their wait? - 2.
Over the corpus, the CLI ratio is 10.9× but the in-process ratio 5.26×. What explains most of the difference?
- 3.
The Rust CLI’s median was 7–15 ms per program in the matrix, and 3–4 ms in the author’s later runs of the same binary. What does that tell you?
- 4.
Why is the in-process figure for the TS engine taken after 50 warm-up calls?
- 5.
A build compiles 10,000 files. Which matters most?
- 6.
Writing to the VM’s disk, 4 threads gave only 1.24×. Why?
References
- [1] The Cargo Team. Profiles (The Cargo Book). The Cargo Book. doc.rust-lang.org/cargo/reference/profiles.html (checked 2026-10-31)