ADR-001: JavaScript runtime — QuickJS (rquickjs)
Status: accepted
Context
Tests embed JavaScript three ways (inline ${js:}, script steps, full
modules). Candidates: QuickJS (via rquickjs), embedded V8 (via
deno_core/rusty_v8), JavaScriptCore, Bun, and pure-Rust engines (Boa).
Decision
QuickJS via rquickjs, behind the ScriptEngine/VuScript traits in
loadr-core so the choice stays reversible.
Rationale
Per-VU isolation cost dominates. k6 semantics require an isolated JS context per VU — no shared mutable state. A load test may run thousands of VUs. QuickJS runtimes cost kilobytes and microseconds to create; V8 isolates cost megabytes and milliseconds. At 5 000 VUs that's the difference between "fine" and gigabytes of heap before the first request.
Execution model. Load scripts are straight-line blocking code
(http.get(), sleep(), check()). QuickJS lets host calls block
synchronously and bridge into Tokio (block_in_place + block_on).
deno_core imposes its async ops/event-loop model, which fights
deterministic pacing and per-request hooks.
Distribution. rusty_v8 means huge prebuilt artifacts, long CI builds
and friction on musl/distroless/cross targets — directly against the
single-small-binary goal. QuickJS is plain C, compiled by cargo everywhere.
The JIT doesn't pay here. Iterations are network-bound; script time is a few percent of iteration time. This mirrors k6's own choice of goja (a non-JIT Go interpreter) over embedding V8. Hot paths (crypto, encoding, HTTP, JSON) are native Rust functions exposed to JS.
Why not the others:
- Bun is a runtime/toolkit on JavaScriptCore written in Zig — there is no
supported way to embed it in a Rust process. Using it would mean shipping a
separate
bunexecutable and IPC, destroying the single-binary story, per-VU isolation and the platform matrix. JavaScriptCore itself has only immature Rust bindings with painful static linking on Linux. - Boa (pure Rust): attractive supply-chain-wise, but slower than QuickJS with less complete ES support at decision time.
Consequences
- CPU-heavy user script code runs ~10–30× slower than V8 — documented; native stdlib mitigates the common cases.
- Sandboxing is straightforward: per-runtime memory limits + interrupt
handler for wall-clock budgets (
js.timeout,js.memory_limit_mb). - A V8-backed
ScriptEngineimplementation can slot in later behind the same trait if benchmarks ever justify it.