EarendilEarendildjsHow djs works
Source

Benchmarks

How fast does djs start, run and dump the scripts an agent writes, and what does it cost in download and memory, next to the QuickJS builds a host would otherwise embed? This page compares them on workloads modeled on real agent codemode sessions, all in WebAssembly, the way a host runs them, and says how to re-run every number.

In short. Seven engines (djs, four QuickJS builds of both QuickJS lines, and monty, a Python subset) run nine codemode workloads in wasm in one node process, each run in a fresh VM, and must all return the same result. djs starts a VM fastest and has the lowest run time over the set; it is ahead where scripts move JSON, strings and tool results, and behind on the smallest bodies and on some single string builtins. Passing data through the host bridge, each engine its best way, djs takes values in about twice as fast and reads them out about ten times as fast; it is behind on bytes coming in and on many tiny calls. Its module is the largest of the JavaScript engines and it takes more memory than the QuickJS releases built here, and its dumps of a paused run are kilobytes, written or loaded in under a millisecond for most workloads. scripts/bench-engines.sh fetches, builds and runs everything; the tables and charts are generated from the recorded results.

What is measured, and why this way

An agent’s codemode body is short and makes a few tool calls: it reads files, runs commands, reshapes their output and prints a summary. The engine’s own work is small, so what a host pays is mostly starting a VM, passing tool results in and out, parsing the body and assembling strings. The comparison measures exactly that, as an embedder sees it:

  • Everything in wasm, in one host. Every engine is a wasm module run by node: the build a host would ship, not a native binary. Each module is compiled once per process, as a host keeps a compiled module.
  • A fresh VM per run. Every run gets a new instance and VM, isolated from the others, as a host gives each tool call its own sandbox. New VM is the time to get one ready; run is the time from handing it the script to having the result string.
  • A warm process. The times are medians of 25 runs after 3 warm-up runs, so node has optimized each engine’s code, as in a host that has been running for a while.
  • The same tool bridge. Tool answers are recorded once and handed to every engine as one JSON string; inside the engine, each tool call turns its arguments into JSON to find its answer and parses the answer’s JSON text. That is the work a host bridge does, and it is the same code in every engine, so the comparison measures the engines and not their host interfaces. Passing data through the bridge compares the host interfaces.
  • The same answer. Every engine’s result must be the string node computes for the same script, or the run fails.
  • Memory is the instance’s linear memory after the run. Wasm memory never shrinks, so that is the run’s high-water mark, allocator overhead included.

Times depend on the machine and its load; the tables say when and at what load they were taken. Instruction counts would not compare across engines with different hosts, so the page gives wall-clock medians.

The engines

enginewhat it ishow it is built here
djsthis engine, the shipped modulescripts/build-wasm.sh
quickjs-wasiQuickJS-NG for WASI with snapshot support, version 3.6.2the npm package’s quickjs.wasm
QuickJSBellard’s QuickJS 2026-06-04, the latest releasewasi-sdk 34, -O2, a minimal embedding
QuickJS-NGQuickJS-NG 0.18.0, the latest releasethe same: wasi-sdk 34, -O2, the same embedding
quickjs-emscriptenthe popular emscripten build, version 0.32.0, of Bellard’s QuickJS 2025-09-13the npm variant @jitl/quickjs-wasmfile-release-sync
quickjs-emscripten (NG)the same package with QuickJS-NG 0.12.1the npm variant @jitl/quickjs-ng-wasmfile-release-sync
montypydantic’s Python subset for code written by models, version 1.3.0the npm package’s wasm component

The two latest QuickJS releases are built by the script with one compiler, the same flags and the same small embedding (js/bench/engines/driver.c): a runtime, a context, an evaluation that runs the job queue dry, and a result string. monty runs the workloads translated to Python by hand; its component is instantiated on the calling thread with no filesystem, environment or network, as its own worker does.

The workloads

The workload set (tests/bench/codemode/) was derived from about three thousand codemode bodies of agent sessions: they were grouped into clusters by what they do, and each cluster got one synthetic body of its typical size and operation mix. The data the tools return is invented: source-like files, command output, search results, query rows, an image.

workloadshare of bodieswhat it does
sequential30%a read, an edit batch with large literal arguments, two commands
fanout22%twelve reads and three checks with Promise.all, joined and cut to an output budget
media13%render a view, read the image, hand a 200 KB data URL to the host
inspect9%no tools: group and search the tool catalog
loop8%eight searches one after another, each result formatted with a regular expression
text8%parse grep-like output, group by file, scan files for markers and definitions
json7%group 600 query rows, rewrite a package manifest
ts2%the json shape in TypeScript (djs runs it as is; the other engines get it with the types stripped by the host)
heavythe largest 1%97 calls: 96 file reads in batches with per-file regular expression work

A typical body is about 500 characters and makes three tool calls; the workloads sit at or a little above their cluster’s median, so that the engines’ work shows.

Download size

What a host ships: the wasm module, and the JavaScript some builds need around it.

Module size per engine, raw and with gzip, in KB on a logarithmic scale Horizontal bars per engine: the wasm module's bytes and its gzip -9 size. The table below has the exact numbers. djs quickjs-wasi QuickJS QuickJS-NG quickjs-emscripten quickjs-emscripten (NG) monty wasm, KB 1,000 10,000 980 637 966 1,087 503 529 24,593 gzip, KB 100 1,000 10,000 427 287 332 371 231 247 7,628
Module size per engine, raw and with gzip, in KB on a logarithmic scale

enginewasmgzipbrotliJS glue, gzip
djs980 KB427 KB360 KBnone
quickjs-wasi637 KB287 KB247 KB28 KB
QuickJS966 KB332 KB262 KBnone
QuickJS-NG1,087 KB371 KB295 KBnone
quickjs-emscripten503 KB231 KB199 KB8 KB
quickjs-emscripten (NG)529 KB247 KB212 KB8 KB
monty24,593 KB7,628 KB4,804 KB55 KB

Module bytes as shipped; gzip -9, brotli quality 11; measured at 1268c57eeb, 2026-11-06, load 2.3.

djs’s module is the largest of the JavaScript engines: it carries the TypeScript front end and the base image of an initialized realm (every builtin object, coded to

46 KB), which spares each new VM from building them.

quickjs-wasi and the emscripten builds are optimized for size; the two releases built here use -O2, as djs’s module is optimized for speed. monty’s module is a different order of size altogether: a Python interpreter with its standard library subset and a type checker.

Starting a VM

A host that gives every call a fresh VM pays this on every call.

Compiling the module, starting a VM and an idle VM's memory, per engine Horizontal bars per engine: compile time in ms and idle memory in MiB on logarithmic scales, new-VM time in ms on a linear one. The table below has the exact numbers. djs quickjs-wasi QuickJS QuickJS-NG quickjs-emscripten quickjs-emscripten (NG) monty compile, ms 1 10 1.03 2.94 3.27 1.93 1.28 6.7 30.5 new VM, ms 0 0.2 0.4 0.6 0.124 0.262 0.204 0.261 0.238 0.281 0.543 idle memory, MiB 0.1 1 10 0.6 1.3 0.3 0.4 16.0 16.0 3.9
Compiling the module, starting a VM and an idle VM’s memory, per engine

enginecompile, msnew VM, msidle VM memory, MiB
djs1.030.120.6
quickjs-wasi2.940.261.3
QuickJS3.270.200.3
QuickJS-NG1.930.260.4
quickjs-emscripten1.280.2416.0
quickjs-emscripten (NG)6.700.2816.0
monty30.50.543.9

Compile: the module, once per process. New VM: a new instance and VM, ready to evaluate (median). Memory: linear memory; measured at 1268c57eeb, 2026-11-06, load 2.3.

A new djs VM is ready in 0.12 ms, against

0.20 ms for QuickJS and 0.26 ms for QuickJS-NG: the realm

comes from the base image in the module, so creating a VM copies an image instead of running the code that builds every builtin (the snapshots page says how). Its idle memory is above the QuickJS releases’: the base image is larger than the heap a fresh QuickJS context allocates, and djs reserves a larger native stack, which sets how deeply its parser may nest. The emscripten builds start at 16 MiB, emscripten’s initial heap, whatever runs in them.

Run time

The time from handing a fresh VM the script and its tool answers to having the result: parsing and compiling the body, the tool calls through the bridge, and the script’s work.

Run time per workload and engine, in ms on a logarithmic scale Grouped columns: one group per workload, one column per engine, lower is better. The table below has the exact numbers. djs quickjs-wasi QuickJS QuickJS-NG quickjs-emscripten quickjs-emscripten (NG) monty 0.1 0.2 0.5 1 2 5 10 20 50 ms sequential fanout media inspect loop text json ts heavy
Run time per workload and engine, in ms on a logarithmic scale

workloaddjsquickjs-wasiQuickJSQuickJS-NGquickjs-emscriptenquickjs-emscripten (NG)monty
sequential0.440.360.360.360.410.380.62
fanout1.031.631.241.151.791.681.43
media0.290.490.910.451.490.980.82
inspect0.260.230.200.200.280.310.63
loop0.762.650.842.111.104.411.67
text1.441.681.501.461.912.392.91
json1.672.131.811.892.652.753.55
ts2.042.341.992.122.943.002.70
heavy11.615.715.316.018.829.411.8
relative to djs1.00×1.36×1.17×1.20×1.59×1.91×1.77×

Milliseconds per run (median), in a warm process; the last row is the geometric mean of each engine’s time over djs’s; measured at 1268c57eeb, 2026-11-06, load 2.3.

djs has the lowest time over the set, and the largest leads where a body moves data: fanout, json and media pass large tool results through JSON.parse and strings (djs parses and writes JSON in about a third of QuickJS’s time, see the operations below), and heavy spends most of its time splitting, matching and scanning file texts. loop is a regular expression replace over prose in a loop, where Bellard’s QuickJS is close and QuickJS-NG and quickjs-wasi are far behind. djs is behind on the shortest bodies, sequential and inspect, by a few hundredths of a millisecond: compiling the body costs about the same in both, and what is left is a handful of small builtin calls (Object.entries, toFixed), each dearer in djs (below). On ts djs compiles the TypeScript itself, while the other engines get JavaScript whose types the host stripped beforehand, outside the timing. monty is fast on heavy, whose string splitting and regular expressions are native in its Rust implementation, and slower on the rest; its times include the same JSON bridge in Python.

Memory

The instance’s linear memory after the run: what the run cost, since wasm memory does not shrink.

Linear memory after the run per workload and engine, in MiB on a logarithmic scale Grouped columns: one group per workload, one column per engine, lower is better. The table below has the exact numbers. djs quickjs-wasi QuickJS QuickJS-NG quickjs-emscripten quickjs-emscripten (NG) monty 0.2 0.5 1 2 5 10 20 MiB sequential fanout media inspect loop text json ts heavy
Linear memory after the run per workload and engine, in MiB on a logarithmic scale

workloaddjsquickjs-wasiQuickJSQuickJS-NGquickjs-emscriptenquickjs-emscripten (NG)monty
sequential0.71.40.40.416.016.05.1
fanout1.82.51.61.616.016.05.6
media1.02.11.11.116.016.05.5
inspect0.61.30.40.416.016.05.1
loop0.91.50.50.616.016.05.1
text1.21.60.60.716.016.07.1
json1.42.21.11.316.016.05.9
ts1.62.21.11.216.016.05.8
heavy4.63.42.42.416.016.07.2
relative to djs1.00×1.53×0.68×0.72×12.62×12.62×4.54×

Linear memory after the run, MiB; the last row is the geometric mean of each engine’s over djs’s; measured at 1268c57eeb, 2026-11-06, load 2.3.

djs needs more memory than the QuickJS releases built here and less than quickjs-wasi, monty and the emscripten builds. Part of the gap is constant: the larger idle footprint above. The rest comes from the garbage collector. djs’s collector traces the live heap when the allocation since the last collection crosses a threshold, about as large again as what survived, so garbage stays until then; QuickJS counts references and frees most garbage the moment it is dropped. heavy, which allocates a few megabytes of short-lived lines and strings, shows it most.

Operation by operation

Single operations on data shaped like the workloads’, each in a loop, against the faster of the two QuickJS releases. Above 1, djs is slower.

djs's time over the best QuickJS build's, per operation, on a logarithmic scale around parity Bars from the parity line at 1: to the left djs is faster, to the right slower. The table below has the times. ← djs faster djs slower → 1× 2× 0.5× 4× 0.25× JSON.parse of 600 query rows 0.35 JSON.stringify of 600 query rows 0.30 split("\n") of file texts 0.44 join("\n") of their lines 0.67 includes per line 1.37 template literal appends 1.30 trim 3.16 slice 2.04 padStart 2.71 toLowerCase 1.78 charCodeAt loop 1.59 String(i) 1.86 toFixed 1.97 Math.max 1.79 Math.floor 2.17 match(/function \w+/g) 0.42 replace(/\s+/g, " ") 1.04 exec of a line pattern 0.67 matchAll with /gm 0.91 new RegExp 0.93 map 0.98 sort with a comparator 1.93 sort of strings 0.97 push 1.09 Map.set with string keys 1.84 Set.add with string keys 1.55 object spread 1.47 Object.entries and fromEntries too short to compare await of a value 0.32 Promise.all of 20 0.56
djs’s time over the best QuickJS build’s, per operation, on a logarithmic scale around parity

operationdjs, msQuickJS, msdjs / QuickJS
JSON.parse of 600 query rows0.992.860.35
JSON.stringify of 600 query rows0.933.100.30
split("\n") of file texts1.172.660.44
join("\n") of their lines0.460.690.67
includes per line3.092.271.37
template literal appends5.484.221.30
trim11.13.503.16
slice7.553.712.04
padStart8.243.042.71
toLowerCase18.410.31.78
charCodeAt loop10.36.461.59
String(i)6.073.261.86
toFixed5.822.951.97
Math.max0.670.381.79
Math.floor5.742.642.17
match(/function \w+/g)2.115.020.42
replace(/\s+/g, " ")7.997.691.04
exec of a line pattern9.1613.60.67
matchAll with /gm4.705.150.91
new RegExp1.581.710.93
map3.443.520.98
sort with a comparator11.25.791.93
sort of strings2.272.350.97
push8.387.691.09
Map.set with string keys18.510.01.84
Set.add with string keys10.86.971.55
object spread6.404.351.47
Object.entries and fromEntries0.380.00-
await of a value5.4016.90.32
Promise.all of 204.447.890.56

Milliseconds per loop of the operation (js/bench/engines/ops.ts), the setup’s time subtracted; QuickJS: the faster of the two latest releases; measured at 1268c57eeb, 2026-11-06, load 2.0.

JSON, splitting and joining, regular expression matching, sorting strings, await and Promise.all are faster in djs. What is still slower is a group of small string and number builtins (trim, padStart, slice, toLowerCase, String(i), toFixed, Object.entries, Math.floor): djs writes most builtins in a TypeScript subset compiled to its own bytecode, and although the common ones take a fast path, a call still costs more instructions than a call of QuickJS’s C functions, which shows when the operation itself is tiny. sort with a comparator runs the comparator, a script function, for every comparison, and calling it from the sort still costs more than in QuickJS; Map.set and Set.add with string keys also take more instructions per call. These matter in a loop of tens of thousands of calls; in the workloads they are a small part of the time.

Passing data through the bridge

Moving data between the host and the sandbox is what cheap data passing is about: a tool result going in, a run’s result or a tool call’s arguments coming out. The workloads above pass every tool answer the same way in every engine, as JSON text, so that they compare the engines. This section instead compares the bridges: each engine gets the best path its embedding offers (js/bench/engines/bridge.ts).

enginevalue invalue outa tool call
djsthe host encodes the value in the wire format, the VM decodes it into cells; a long ASCII string is copied straight into a string the VM reservedthe reader decodes the value out of the arena in place; the VM does nothingthe call’s record names the arguments; the reader reads them, the result goes in as above
QuickJS, QuickJS-NGJSON.stringify on the host, the UTF-8 written into linear memory, JS_ParseJSON there; strings by JS_NewStringLen, bytes as a copied ArrayBufferJS_JSONStringify in the VM and its UTF-8 read in place, then JSON.parse; strings as their UTF-8, bytes by pointera C function serializes its argument with JS_JSONStringify and calls the host, which answers with JSON text or a string
quickjs-wasiJSON text parsed by JSON.parse in the VM, or its handle walk (hostToHandle)JSON.stringify in the VM, or its handle walk (dump)newFunction with the same conversions
quickjs-emscriptenJSON text parsed in the VM, or objects built through handlesdump, which is JSON.stringify in the VM and JSON.parse on the hostnewFunction with the same conversions
montyJSON text and json.loads, or its value arena, which the component model lowers into the modulejson.dumps, or the value arena lifted outan external function: the run stops, the host answers with a value arena

Where an engine has two paths, each number below is the faster one’s: the handle walks of quickjs-wasi and quickjs-emscripten win on arrays of numbers, whose decimal text is slow to parse, and lose on most other values, and monty’s JSON text beats its value arena for anything larger than call arguments. The payloads: call arguments; a search tool’s result as an object, of 1 KB, 100 KB, 1 MB and 10 MB of JSON; source text of 1 MB and 10 MB (ASCII) and 1 MB of prose with accented letters, dashes and CJK; 100,000 floating-point numbers; a 1 MiB Uint8Array; and objects nested 100 deep (monty’s json.loads stops at about 200). Every value must read back equal to what went in. Each time is the median of one operation in fresh VMs; small payloads repeat within a sample.

In

Putting a value into the VM, per payload and engine, in ms on a logarithmic scale Grouped columns: one group per payload, one column per engine, lower is better. The table below has the exact numbers. djs quickjs-wasi QuickJS QuickJS-NG quickjs-emscripten quickjs-emscripten (NG) monty 0.001 0.01 0.1 1 10 100 ms call arguments JSON, 1 KB JSON, 100 KB JSON, 1 MB JSON, 10 MB ASCII, 1 MB ASCII, 10 MB non-ASCII, 1 MB 100k numbers bytes, 1 MiB nested 100 deep
Putting a value into the VM, per payload and engine, in ms on a logarithmic scale

payloadsizedjsquickjs-wasiQuickJSQuickJS-NGquickjs-emscriptenquickjs-emscripten (NG)monty
call arguments102 B0.0020.0020.0010.0010.0030.0030.025
JSON, 1 KB1 KB0.0060.0090.0080.0080.0150.0210.052
JSON, 100 KB100 KB0.3130.770.6420.7051.11.180.816
JSON, 1 MB1,000 KB3.137.366.3571111.97.79
JSON, 10 MB10,000 KB31.280.972.778.211712897.5
ASCII, 1 MB1,000 KB0.1450.4040.390.3913.633.732.23
ASCII, 10 MB10,000 KB1.054.043.783.7736.336.822.6
non-ASCII, 1 MB1,000 KB1.84.992.673.265.396.182.3
100k numbers1,076 KB1.023.668.7920.88.59146.49
bytes, 1 MiB1,049 KB0.1930.1410.1430.150.0840.0830.091
nested 100 deep4 KB0.020.0360.0280.0320.050.0530.089
relative to djs1.0×2.0×1.9×2.1×3.9×4.4×4.3×

Milliseconds per value put into a fresh VM (median), each engine’s best path; the last row is the geometric mean of each engine’s time over djs’s; measured at 1268c57eeb, 2026-11-06, load 2.0.

djs takes JSON-like values in about half the time of the fastest QuickJS path: a 1 MB search result in 3.1 ms against

6.4 ms for QuickJS. The host writes the value once in the wire

format and the VM builds every object at its final size in one pass, where the others serialize it to JSON text on the host and parse that text again in the VM. A long ASCII string is one copy into a string the VM reserved for it (1.0 ms for 10 MB, against 3.8 ms for QuickJS, which first encodes the text into its linear memory and then copies it into a string). Text with non-ASCII characters gains less: it is encoded to UTF-8 and transcoded back in the VM. Numbers gain as much, since the wire format carries them as binary values instead of decimal text to be parsed.

Bytes are where djs is behind: a Uint8Array is copied into the encoder’s buffer, then into the staging buffer, then into the VM’s own bytes, three copies where the others make two. The emscripten builds pay the most for strings, because their glue encodes them to UTF-8 in JavaScript, byte by byte.

Out

Reading a value out of the VM, per payload and engine, in ms on a logarithmic scale Grouped columns: one group per payload, one column per engine, lower is better. The table below has the exact numbers. djs quickjs-wasi QuickJS QuickJS-NG quickjs-emscripten quickjs-emscripten (NG) monty 0.001 0.01 0.1 1 10 100 ms call arguments JSON, 1 KB JSON, 100 KB JSON, 1 MB JSON, 10 MB ASCII, 1 MB ASCII, 10 MB non-ASCII, 1 MB 100k numbers bytes, 1 MiB nested 100 deep
Reading a value out of the VM, per payload and engine, in ms on a logarithmic scale

payloadsizedjsquickjs-wasiQuickJSQuickJS-NGquickjs-emscriptenquickjs-emscripten (NG)monty
call arguments102 B0.0010.0030.0020.0020.0040.0050.022
JSON, 1 KB1 KB0.0020.0170.0110.0150.0360.0440.027
JSON, 100 KB100 KB0.1271.290.8991.22.763.410.534
JSON, 1 MB1,000 KB1.2312.58.541228.735.15.18
JSON, 10 MB10,000 KB13.21269012228535251.9
ASCII, 1 MB1,000 KB0.0791.20.3160.2660.6540.9060.2
ASCII, 10 MB10,000 KB0.13111.42.582.576.418.931.44
non-ASCII, 1 MB1,000 KB0.0362.541.781.691.972.30.871
100k numbers1,076 KB0.418.4717.919.552.499.98.87
bytes, 1 MiB1,049 KB0.0530.0330.0650.030.0970.0970.192
nested 100 deep4 KB0.0110.0770.0590.070.180.2330.053
relative to djs1.0×10.7×7.2×7.8×18.0×22.9×7.5×

Milliseconds per value read out of a VM (median), each engine’s best path; the last row is the geometric mean of each engine’s time over djs’s; measured at 1268c57eeb, 2026-11-06, load 2.0.

Reading out is where the designs differ most. djs’s reader decodes the value straight out of the VM’s memory, and the VM does no work at all; everyone else first has the VM write the value as JSON text, copies the text out and parses it on the host. A 1 MB search result comes out of djs in 1.23 ms, against

8.5 ms for QuickJS and 5.2 ms for monty, whose

json.dumps is the fastest of the JSON paths here. A string comes out at the speed of a memory copy, since its characters are already in the form a host string needs. A Uint8Array is copied out in all of them; the reader’s copy costs a few hundredths of a millisecond more than reading the bytes in place by a pointer.

Throughput and memory

engineJSON 1 MB, inoutJSON 10 MB, inoutASCII 10 MB, inoutnon-ASCII 1 MB, inoutbytes 1 MiB, inout
djs3208113217569,52776,22055527,8555,42719,935
quickjs-wasi13680124792,4758762003937,44731,872
QuickJS1571171381112,6463,8683755637,35316,058
QuickJS-NG14383128822,6553,8843075936,96734,493
quickjs-emscripten913586352751,56018650612,54310,777
quickjs-emscripten (NG)842978282711,12016243412,60310,777
monty1281931031934436,9594351,14811,5485,458

MB per second: the payload’s size over the median time; measured at 1268c57eeb, 2026-11-06, load 2.0.

engineidleJSON 10 MB inand outASCII 10 MB inand out
djs0.644.644.610.110.1
quickjs-wasi1.342.252.020.420.4
QuickJS0.336.756.619.419.4
QuickJS-NG0.441.351.119.419.4
quickjs-emscripten16.042.083.124.224.2
quickjs-emscripten (NG)16.050.483.124.224.2
monty5.082.6108.614.532.6

Linear memory, MiB: of a fresh VM, after putting the value in, and after reading it back out; measured at 1268c57eeb, 2026-11-06, load 2.0.

The memory after putting a 10 MB value in is mostly the value itself in each engine’s representation. djs keeps a little more than QuickJS for JSON: its staging buffer stays as large as the largest wire encoding it was given. Reading the value out adds nothing in djs, while the engines that go through JSON text need room for the text, which in the emscripten builds doubles their memory. A 10 MB ASCII string takes about its own size in djs, one copy; in QuickJS it is there twice, as the UTF-8 the host wrote and as the string.

End to end

Three whole runs, each in a fresh VM, with a host function tool that every engine calls synchronously: a 1 MB search result of which the script returns one path, a 1 MB file that the script edits with one replace and passes to a second call, and a thousand calls with small arguments and results.

Whole runs that pass data through a tool, per engine, in ms on logarithmic scales Horizontal bars per engine, one panel per case: a tool's 1 MB result of which the script reads one field, a 1 MB file read, edited and written back, and a thousand small calls. Lower is better. The table below has the exact numbers. djs quickjs-wasi QuickJS QuickJS-NG quickjs-emscripten quickjs-emscripten (NG) monty one field of 1 MB 1 2 5 10 20 50 3.25 7.34 6.68 7.43 11.7 12.8 29.5 edit a 1 MB file 0.1 1 10 0.303 1.68 3.98 5.56 11.9 11.7 2.89 1,000 small calls 0.5 1 2 5 10 20 2.88 3.06 1.7 2.08 5.24 5.61 19.9
Whole runs that pass data through a tool, per engine, in ms on logarithmic scales

casedjsquickjs-wasiQuickJSQuickJS-NGquickjs-emscriptenquickjs-emscripten (NG)monty
one field of 1 MB3.257.346.687.4311.712.829.5
edit a 1 MB file0.3031.683.985.5611.911.72.89
1,000 small calls2.883.061.72.085.245.6119.9

Milliseconds per run in a fresh VM (median): compiling and running the script, its tool calls through each engine’s host function; measured at 1268c57eeb, 2026-11-06, load 2.0.

The case of a 1 MB result of which the script reads one field is decided by putting the result in: every engine, djs included, builds the whole value before the script runs, so djs’s lead is that of the table above, and reading one string out costs nothing anywhere. Editing a 1 MB file shows both directions: the file goes in as one copy, and the edited text comes out to the second call through the reader, in

0.30 ms against 4.0 ms for QuickJS, whose host function has to

write the megabyte as JSON text inside the VM and parse it again on the host.

A thousand small calls are where djs is behind the minimal C embedding:

2.9 ms against 1.7 ms. With small values the conversion is

cheap everywhere, and what remains is the call itself: djs’s run returns to the host at every call, the host reads the call’s record, puts the result in and runs the VM again, while the C embedding calls the host function from inside the interpreter (the other QuickJS builds do too, but every value crosses through their JavaScript glue, and they are slower than djs). monty stops and resumes its run for every call, which costs it 20 ms.

In short, djs’s two halves pay off differently. The wire format makes the way in about twice as fast as JSON text for structured data and a single copy for long ASCII strings. The reader makes the way out an order of magnitude faster, because no engine work and no text happen in between. Bytes coming in and many tiny calls are the places where the simpler paths of the C embedding are still ahead.

Dumps

djs can write a run that waits on tool calls as a continuation dump and continue it later in a new sandbox. Here every workload runs through the sandbox with asynchronous host tools and is dumped at every idle point, every time it waits on a call:

djs's continuation dumps per workload: size, and the time to write and to load one Horizontal bars per workload: the median dump size in KiB on a logarithmic scale, the median write and load times in ms. A workload that never waits has no dump. The table below has the exact numbers. sequential fanout media inspect loop text json ts heavy dump, KiB 1 10 100 1.2 74.3 0.4 none 8.6 52.2 121.9 1.7 42.1 write, ms 0 1 2 3 4 0.296 0.511 0.13 none 0.462 0.758 3.91 0.737 0.486 load, ms 0 1 2 3 0.298 0.622 0.226 none 0.351 0.889 2.96 0.369 0.66
djs’s continuation dumps per workload: size, and the time to write and to load one

workloadidle pointsdump, KiB (max)gzip, KiB (max)write, msload, ms
sequential41.2 (2.3)0.6 (0.9)0.300.30
fanout1574.3 (139.1)17.1 (33.5)0.510.62
media20.4 (0.4)0.3 (0.3)0.130.23
inspect0
loop88.6 (15.5)3.1 (5.2)0.460.35
text552.2 (53.0)9.2 (9.4)0.760.89
json3121.9 (122.0)20.5 (20.6)3.912.96
ts11.7 (1.7)0.8 (0.8)0.740.37
heavy9742.1 (114.9)10.3 (25.2)0.490.66

Medians over a workload’s idle points; load: a new sandbox with the run loaded; measured at 1268c57eeb, 2026-11-06, load 2.3.

Most dumps are a few kilobytes, and gzip takes the larger ones to between a fifth and a third; they hold the run’s live state, so a dump taken while json holds 600 query rows, or while fanout holds the file texts it has read, is as large as that data. Writing one and loading it into a new sandbox takes about as long as starting a VM and running a small body; the json dumps take longest, as they encode and decode the most objects. inspect makes no tool call, so it never waits and has nothing to dump.

Re-running

scripts/bench-engines.sh          # fetch and build the engines, run, print the tables
scripts/bench-engines.sh --setup  # fetch and build only
scripts/status.py engines         # three runs; record the medians for this page
scripts/status.py bridge          # only the bridge benchmark
cargo run -q -p djs-docs -- regen # rewrite the tables

The engines go into vendor/engines of the primary checkout: npm packages pinned with their integrity hashes by js/bench/engines/package-lock.json, wasi-sdk and the QuickJS release sources pinned by SHA-256 in scripts/bench-engines.sh. Run it on a quiet machine: the harness prints the load it started at, and recorded numbers should come from a load below about 4.

Where to look