Size and speed
Two independent things make a slim binary small:
- Removing features —
BUILD_WITH_WASM=OFF,BUILD_WITH_TLS=OFFand friends. Large, predictable, and it changes what the runtime can do. - Changing how the same code is compiled —
-Oz, LTO, dead-code stripping, identical code folding. Smaller, and it changes only how fast the runtime is.
Published profiles combine both. Downloads lists what you can get prebuilt; Slim builds documents every option.
Every number here carries the platform, date and commit it was measured at. The same build on a different toolchain can move by 3%, so a bare percentage without provenance is worse than no table at all.
Released size history
Exact bytes from published ZIP files. Hover chart for exact values. Toggle platform, unit, or profile.
Exact released sizes
| release | profile | platform | raw |
|---|---|---|---|
slim-v26.6.0-8 | balanced-min | Linux x86-64 | 1,947,800 B |
slim-v26.6.0-6 | min | Linux x86-64 | 1,914,824 B |
slim-v26.6.0-7 | min | Linux x86-64 | 1,918,920 B |
slim-v26.6.0-8 | min | Linux x86-64 | 1,914,824 B |
slim-v26.6.0-7 | ffi-tls-sqlite | Linux x86-64 | 3,205,248 B |
slim-v26.6.0-8 | ffi-tls-sqlite | Linux x86-64 | 3,209,344 B |
What each feature costs
Paired builds differ by one removable capability. Bars cannot be summed because linked code overlaps.
Cost is linked section bytes. Executable file size is shown beside it and is usually smaller: segments are padded to a page boundary, so a saving under ~16 KB may not move the download at all.05707539-macos-arm64-feature-study-v1 · 05707539 · macOS arm64 · Release build, every removable feature ON, one switch turned off per pair.Exact paired sizes
feature without with cost file size delta SQLite 3,969,210 B 5,400,609 B +1,431,399 B +1,523,296 B TLS 4,930,746 B 5,400,609 B +469,863 B +513,376 B WebAssembly 5,044,956 B 5,400,609 B +355,653 B +422,048 B mimalloc 5,247,370 B 5,400,609 B +153,239 B +172,032 B Bundled CA 5,291,657 B 5,400,609 B +108,952 B +99,632 B Full WAMR 5,330,636 B 5,400,609 B +69,973 B +66,976 B FFI 5,331,702 B 5,400,609 B +68,907 B +74,880 B WebCrypto 5,345,465 B 5,400,609 B +55,144 B +63,696 B REPL 5,386,469 B 5,400,609 B +14,140 B +608 B bundle command 5,393,313 B 5,400,609 B +7,296 B +48 B XMLHttpRequest 5,394,017 B 5,400,609 B +6,592 B +224 B test command 5,394,657 B 5,400,609 B +5,952 B +-128 B app command 5,397,281 B 5,400,609 B +3,328 B +224 B help text 5,399,713 B 5,400,609 B +896 B +192 B serve command 5,399,777 B 5,400,609 B +832 B +144 B compile command 5,399,841 B 5,400,609 B +768 B +48 B eval command 5,400,481 B 5,400,609 B +128 B +192 B --tls-ca option 5,400,481 B 5,400,609 B +128 B +96 B
Each bar is one pair of builds that differ by a single switch, from a baseline with every feature on — measured on macos-arm64, the only platform whose toolchain supports every size lever this fork has. Use the selector to compare it against deltas derived from released profiles, which cover fewer features but exist for all four platforms.
Sizes in the table below are from BUILDINFO.txt inside each published artifact of
slim-v26.6.0-8 (commit 1274e5e7, 2026-08-24), linux-x86_64, unpacked and stripped.
| profile | binary | vs min |
|---|---|---|
min | 1,914,824 B | — |
ffi | 1,964,424 B | +48 KB |
tls | 2,365,536 B | +440 KB |
sqlite | 2,717,208 B | +784 KB |
ffi-tls | 2,406,944 B | +481 KB |
ffi-tls-sqlite | 3,209,344 B | +1,264 KB |
A default upstream build is 8,900,848 B, so min is 0.22× of it. Most of that gap is
WebAssembly: WAMR is compiled out of every published profile.
Why these deltas are not additive, and where the numbers come from
The bars are linked code and data, not file size. Executable file size is page-quantized — Mach-O pads segments to 16 KB on arm64 — so it cannot resolve a small feature: the test-runner subcommand costs 5,952 linked bytes, and removing it makes the file 128 bytes bigger. The exact-values table under the chart shows both numbers side by side.
The bars cannot be summed. TLS cannot be removed without the bundled CA and --tls-ca going
with it, so its bar contains all three. WebCrypto measured with TLS on is worth 55,144 B, because
libmbedcrypto stays linked for TLS either way — on a no-TLS build the same flag is worth
182,560 B. Linker dead-code elimination moves bytes between neighbours too. A Sankey diagram would
have to assign that shared code to one arm and would invite exactly the addition that does not
hold, which is why this page uses delta bars.
TLS is mbedTLS plus the embedded Mozilla CA bundle; SQLite is the amalgamation. Neither compresses much further — if you need them, you need the bytes.
The upstream figure comes from the benchmark run at slim-v26.6.0-8-4-g709c52a, four commits
later, where min measures the same 1,914,824 B. The toolchain is whatever ubuntu-latest
shipped that day; BUILDINFO.txt does not record a compiler version, so treat the exact GCC
release as unpinned.
The release chart reads committed records generated from published ZIP files. It never
rebuilds an old tag or fetches GitHub during the site build. The matrix has grown over time and
the chart shows that honestly: slim-v26.6.0-6 shipped four profiles, -7 six, -8 eight. A
profile that did not exist yet is a gap, never zero bytes.
What the codegen modes cost
tuned-min and balanced-min are the min feature set at a different optimisation setting.
They exist because -Oz is not free: it costs real interpreter throughput.
Measured on macOS arm64, Apple clang, min feature set, QuickJS workload from the size
study, minimum user-CPU time of 7 runs, 2026-08-24 for
PR #32:
| variant | binary | Δ size | time | Δ time |
|---|---|---|---|---|
smallest — -Oz + LTO | 1,976,112 B | — | 1.29 s | — |
tuned — outliner off | 2,057,888 B | +4.1% | 0.94 s | −27% |
balanced — QuickJS at -Os | 2,107,728 B | +6.7% | 0.75 s | −41% |
| both together | 2,140,416 B | +8.3% | 0.75 s | −41% |
previous balanced — whole binary -Os, no LTO | 2,189,136 B | +10.8% | 0.76 s | −41% |
Prefer balanced. macOS arm64 is the only platform where the three modes genuinely differ —
-Oz is a Clang feature, so on Linux (GCC) and Windows (MSVC) the build is already at -Os
and all three min variants are the same binary. Verified for tuned-min against
slim-v26.6.0-8's SHA256SUMS.txt: same SHA-256 as min on both Linux architectures.
Why balanced wins, and why stacking it with tuned does not
The machine outliner is the expensive part of -Oz. Clang enables it by default at -Oz.
It turns repeated instruction sequences into calls — exactly the wrong trade inside QuickJS's
interpreter dispatch loop. On arm64 it bought 4% of the binary for 27% of the run time.
BUILD_WITH_NO_OUTLINE=ON turns it off, and it is two flags: under LTO codegen happens at
link, so the compile-time -mno-outline is a silent no-op on its own and the option adds a
linker-side -mllvm -enable-machine-outliner=never as well.
Raising just the engine beats turning the outliner off. BUILD_WITH_QJS_SPEED=ON compiles
deps/quickjs at -Os while everything else stays at -Oz. The binary is not uniform:
essentially all JS execution time is in the engine, while libuv, libwebsockets, mbedTLS and ada
are cold once the runtime is up. It survives LTO where -mno-outline does not — -Os and
-Oz are recorded per function in the IR as the optsize and minsize attributes, which the
LTO backend reads at link.
The two do not stack. The machine outliner only runs on minsize functions, and -Os does
not set that attribute, so raising the engine already puts it out of reach. Enabling both cost
+32,688 B for no change in run time, which is why --optimization balanced explicitly sets
BUILD_WITH_NO_OUTLINE=OFF.
The balanced row above is the current recipe. slim-v26.6.0-8 was cut before it landed and
still ships the previous one (whole binary at -Os, LTO off) — on linux-x86_64 that is
1,947,800 B against min's 1,914,824 B, a gap that is entirely the missing LTO and says
nothing about engine optimisation. The next release carries the newer build: smaller at the
same speed on macOS, and identical to min on Linux and Windows.
Continuous benchmarks
Same-run ratios against full build. Lower is faster.
709c52a2 · 2026-08-24 · macos15 · Apple clang version 17.0.0 (clang-1700.0.13.5)Exact benchmark ratios
profile Startup + stdlib direction balanced-min— lower is better tuned-min— lower is better min0.894× lower is better ffi0.833× lower is better tls0.881× lower is better sqlite— lower is better ffi-tls0.848× lower is better ffi-tls-sqlite— lower is better
Sizes alone do not say whether a slim build is slower at anything that matters, so the fork
records benchmark runs: startup, resident memory and throughput workloads, on linux-x86_64
and macos-arm64. From the run at slim-v26.6.0-8-4-g709c52a (2026-08-24) on linux-x86_64,
as ratios of the full build:
| metric | min |
|---|---|
| binary size, raw | 0.22× |
| binary size, gzip -9 | 0.25× |
| baseline RSS | 0.40× |
| peak RSS, event loop | 0.57× |
JSON.parse throughput | 0.63× |
| SHA-256 throughput | 1.07× |
Read the ratios, not the absolute numbers — hosted runners drift with image and hardware. The
JSON.parse row is the honest cost of -Oz: engine work, in the code path the outliner
damages, and the reason tuned-min and balanced-min are published at all. SHA-256 is native
code the engine barely touches; treat its 1.07× as "unaffected", not as a speedup.
The generated report at
benchmarks/README.md
is machine-written from recorded runs and states runner, toolchain and sampling parameters for
every figure. Prefer it over this page for anything current.
When the benchmarks run, and what they do not gate
Runs happen on demand (workflow_dispatch) and on PRs touching benchmarks/** — not on every
release — and history entries are committed by hand, because nothing in this fork's automation
pushes to slim. The first recorded run compares the full build against min, ffi, tls
and ffi-tls. An all dispatch builds all eight published profiles, including SQLite and both
codegen variants; pass release_tag so every binary comes from the exact release commit.
SQLite should not change engine throughput, but measuring that claim beats assuming it.
There is deliberately no regression gate on these numbers yet — the job is report-only. A gate needs several releases of history first, to know what normal run-to-run noise looks like on hosted runners. A metric whose tool is unavailable is recorded as missing, never as zero.
Measuring your own build
bench.mjs takes the binaries to compare explicitly, so nothing is measured by accident:
node benchmarks/bench.mjs \
--binary full=build/tjs \
--binary min=dist/min/tjs \
--binary balanced-min=dist/balanced-min/tjs
node benchmarks/report.mjs # regenerate benchmarks/README.md from recorded runs
node benchmarks/report.mjs --check # verify it is current, without writing
Add --quick for a smoke run (5 spawns, 1 rep). It is explicitly not a valid measurement —
use it to check the harness works, never to compare builds.
Methodology — what is sampled, how many spawns and warmups, and what the results do not prove —
is in
benchmarks/METHODOLOGY.md.