Skip to main content

Size and speed

Two independent things make a slim binary small:

  1. Removing featuresBUILD_WITH_WASM=OFF, BUILD_WITH_TLS=OFF and friends. Large, predictable, and it changes what the runtime can do.
  2. 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

Released artifact size

Exact bytes from published ZIP files. Hover chart for exact values. Toggle platform, unit, or profile.

1.70 MiB2.07 MiB2.44 MiB2.81 MiB3.18 MiBslim-v26.6.0-6slim-v26.6.0-7slim-v26.6.0-8ReleaseSize (MiB)
Exact released sizes
releaseprofileplatformraw
slim-v26.6.0-8balanced-minLinux x86-641,947,800 B
slim-v26.6.0-6minLinux x86-641,914,824 B
slim-v26.6.0-7minLinux x86-641,918,920 B
slim-v26.6.0-8minLinux x86-641,914,824 B
slim-v26.6.0-7ffi-tls-sqliteLinux x86-643,205,248 B
slim-v26.6.0-8ffi-tls-sqliteLinux x86-643,209,344 B

What each feature costs

Marginal feature cost

Paired builds differ by one removable capability. Bars cannot be summed because linked code overlaps.

SQLite+1397.9 KiBTLS+458.9 KiBWebAssembly+347.3 KiBmimalloc+149.6 KiBBundled CA+106.4 KiBFull WAMR+68.3 KiBFFI+67.3 KiBWebCrypto+53.9 KiBREPL+13.8 KiBbundle command+7.1 KiBXMLHttpRequest+6.4 KiBtest command+5.8 KiBapp command+3.3 KiBhelp text+896 Bserve command+832 Bcompile command+768 Beval command+128 B--tls-ca option+128 BAdded linked code and data

05707539-macos-arm64-feature-study-v1 · 05707539 · macOS arm64 · Release build, every removable feature ON, one switch turned off per pair.

Exact paired sizes

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.

featurewithoutwithcostfile size delta
SQLite3,969,210 B5,400,609 B+1,431,399 B+1,523,296 B
TLS4,930,746 B5,400,609 B+469,863 B+513,376 B
WebAssembly5,044,956 B5,400,609 B+355,653 B+422,048 B
mimalloc5,247,370 B5,400,609 B+153,239 B+172,032 B
Bundled CA5,291,657 B5,400,609 B+108,952 B+99,632 B
Full WAMR5,330,636 B5,400,609 B+69,973 B+66,976 B
FFI5,331,702 B5,400,609 B+68,907 B+74,880 B
WebCrypto5,345,465 B5,400,609 B+55,144 B+63,696 B
REPL5,386,469 B5,400,609 B+14,140 B+608 B
bundle command5,393,313 B5,400,609 B+7,296 B+48 B
XMLHttpRequest5,394,017 B5,400,609 B+6,592 B+224 B
test command5,394,657 B5,400,609 B+5,952 B+-128 B
app command5,397,281 B5,400,609 B+3,328 B+224 B
help text5,399,713 B5,400,609 B+896 B+192 B
serve command5,399,777 B5,400,609 B+832 B+144 B
compile command5,399,841 B5,400,609 B+768 B+48 B
eval command5,400,481 B5,400,609 B+128 B+192 B
--tls-ca option5,400,481 B5,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.

profilebinaryvs min
min1,914,824 B
ffi1,964,424 B+48 KB
tls2,365,536 B+440 KB
sqlite2,717,208 B+784 KB
ffi-tls2,406,944 B+481 KB
ffi-tls-sqlite3,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:

variantbinaryΔ sizetimeΔ time
smallest-Oz + LTO1,976,112 B1.29 s
tuned — outliner off2,057,888 B+4.1%0.94 s−27%
balanced — QuickJS at -Os2,107,728 B+6.7%0.75 s−41%
both together2,140,416 B+8.3%0.75 s−41%
previous balanced — whole binary -Os, no LTO2,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.

Numbers ahead of the artifacts

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

Slim-build speed

Same-run ratios against full build. Lower is faster.

0.00×0.30×0.59×0.89×1.19×full 1.00×0.89×min0.83×ffi0.88×tls0.85×ffi-tls× full time

709c52a2 · 2026-08-24 · macos15 · Apple clang version 17.0.0 (clang-1700.0.13.5)

Exact benchmark ratios
profileStartup + stdlibdirection
balanced-minlower is better
tuned-minlower is better
min0.894×lower is better
ffi0.833×lower is better
tls0.881×lower is better
sqlitelower is better
ffi-tls0.848×lower is better
ffi-tls-sqlitelower 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:

metricmin
binary size, raw0.22×
binary size, gzip -90.25×
baseline RSS0.40×
peak RSS, event loop0.57×
JSON.parse throughput0.63×
SHA-256 throughput1.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.