Viscosity
Review
Part XI · Viscosity in Rust · Day 48

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.

About 15 minutes, including the exercises and quiz
By the end you will be able to
  • 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.

Intuition

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.

Definition

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:

Step throughHow fast is the compiler?
Compile times of the TypeScript and Rust engines over the corpus, as whole processes and in-processTS CLI (node)Rust CLI (native)50 ms100 ms150 mssame_name_collections: 159 ms vs 15.34 msopt_in_mode: 144.1 ms vs 12.96 msout_of_bounds: 142.2 ms vs 11.31 msparam_unknown: 141.8 ms vs 14.21 msreassigned: 139.5 ms vs 14.12 msout_of_bounds_write: 135.5 ms vs 12.87 msstring_index: 135.3 ms vs 13.58 mstrailing_annotation: 135.3 ms vs 12.82 msshape_mismatch: 134 ms vs 13.84 mssmall_auto: 132.6 ms vs 11.46 mswrite_rhs_first: 131.5 ms vs 13.34 msno_loops: 129 ms vs 8.07 msinterproc_mixed: 127.7 ms vs 11.25 mssequence_index: 126.2 ms vs 10.86 mstry_catch: 120.5 ms vs 10.74 mscomma_field_name: 120 ms vs 9.65 mswrite_rhs_grows: 119.8 ms vs 14.16 msaliasing_identity: 118.8 ms vs 11.1 msthrow_from_loop: 118.2 ms vs 10.41 msnested_loops: 117.4 ms vs 10 mstyped_column_semantics: 115.6 ms vs 14.51 mssequence_value: 115.2 ms vs 10.64 msrenamed_column_clash: 114.5 ms vs 8.02 msmap_method: 114.2 ms vs 9.75 msupdate_ops: 114.1 ms vs 8.38 mscompound_out_of_bounds: 113.1 ms vs 10.54 msaosoa_bad_block: 112.7 ms vs 10.27 msrecord_shared: 111.9 ms vs 8.72 msalias_forof: 111.5 ms vs 7.25 msnested_rewrites: 111.1 ms vs 10.3 msgrowth: 109.4 ms vs 10.85 msinterproc_ok: 106.2 ms vs 10.64 msescapes_log: 105.8 ms vs 8.3 msstring_field: 105.8 ms vs 10.37 msliteral_init: 104.6 ms vs 9.36 msmixed_columns: 101.8 ms vs 11.17 msbasic_soa: 98.7 ms vs 7.37 msaosoa: 98 ms vs 11.3 msclosure_alias: 90.3 ms vs 8.56 msall_generic: 81.7 ms vs 7.46 ms
40 corpus programs, one fresh process per compile, the way a user runs viscosity build. Median of 5 runs. TS CLI 81.7 to 159.0 ms, Rust CLI 7.3 to 15.3 ms. Per program, 8.0× to 16.0× (median 10.9×).
1/4

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.

Pitfall

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:

Try itPaying for start-up once
// 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
        );
    }
}
Open in Rust Playground ↗

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.

Under the hood

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

ExerciseSum of medians

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!()
}
Open in Rust Playground ↗

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. 1.

    A user runs viscosity build app.js once from a terminal. Which number predicts their wait?

  2. 2.

    Over the corpus, the CLI ratio is 10.9× but the in-process ratio 5.26×. What explains most of the difference?

  3. 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. 4.

    Why is the in-process figure for the TS engine taken after 50 warm-up calls?

  5. 5.

    A build compiles 10,000 files. Which matters most?

  6. 6.

    Writing to the VM’s disk, 4 threads gave only 1.24×. Why?

References

  1. [1] The Cargo Team. Profiles (The Cargo Book). The Cargo Book. doc.rust-lang.org/cargo/reference/profiles.html (checked 2026-10-31)