JSLab.su is an experimental platform for visualizing
how different JavaScript engines (V8, SpiderMonkey, JavaScriptCore, Hermes)
parse, compile, and optimize your code under the hood.
The site lets you:
- Run one snippet on V8, SpiderMonkey, JavaScriptCore and Hermes, and read each engine's bytecode in its own tab; AST and IR are V8 flags.
- Diff a run against the previous run of the same engine β the outputs of two different engines are never diffed against each other.
- Walk the V8 compilation pipeline stage by stage, including deoptimization traces.
- Step through ECMAScript abstract operations (type conversion, equality, and
the
+operator) against the spec text. - Share reproducible code snippets β as links or as embeddable widgets β for educational or research purposes.
Uploading and visualizing engine logs (v8.log) is on the roadmap, not shipped
yet; see Roadmap.
| Engine | Binary | Output types | Notable flags |
|---|---|---|---|
| V8 | d8 |
AST, Ignition bytecode, Maglev/TurboFan code, IC & deopt traces | --print-bytecode, --print-ast, --trace-opt, --allow-natives-syntax |
| SpiderMonkey | js |
Bytecode (dis()) |
--baseline-eager, --ion-eager |
| JavaScriptCore | jsc |
Bytecode (-d) |
β (-d is applied server-side) |
| Hermes | hermes |
Bytecode (-dump-bytecode) |
-O, -strict, -gc-sanitize-handles |
Flags are validated against a per-engine allowlist β the full catalog lives in
packages/engine-runtime/src/flags.ts
and is served over HTTP at GET /api/flags; anything outside it is rejected and
echoed back in meta.droppedFlags.
The bytecode-dumping switches are applied server-side on every run, so you never
pass them yourself: Hermes always runs with -dump-bytecode, JSC always with
-d, and SpiderMonkey always through a dis() wrapper. V8 is the exception β
d8 prints nothing extra unless you ask, so every V8 view is driven by flags.
The fastest way to run the full stack locally β no Kubernetes required.
Prerequisites
- Docker with the Compose plugin
- Node.js 22 (see
.nvmrc) β only needed for development outside the containers
Run
# --recurse-submodules is required: trace-service does not build
# without the engine262 submodule (apps/trace-service/engine262).
git clone --recurse-submodules https://github.com/pavlof01/jslab.git
cd jslab
docker compose up --buildAlready cloned without submodules? Fetch them first:
git submodule update --init --recursiveOnce the containers are healthy, open the frontend at http://localhost:3000.
The API gateway listens on http://localhost:8080, and every service (redis,
engine-v8, engine-hermes, engine-spidermonkey, engine-jsc,
trace-service, api, frontend) runs in its own container with hot-reload
enabled.
JSLab aims to be a compiler explorer for JavaScript engines β
a place to experiment, learn, and visualize the internals of modern JIT compilers.
Goals:
- Provide a visual way to understand how JavaScript is executed.
- Show the bytecode each engine emits, and V8's optimization stages.
- Serve as an educational and research platform for JS internals.
/apps
ββ api # Fastify API gateway (rate limit + cache + engine proxy)
ββ engine-v8 # d8 wrapper HTTP service
ββ engine-hermes # hermes -dump-bytecode wrapper HTTP service
ββ engine-jsc # JavaScriptCore (jsc) wrapper HTTP service
ββ engine-spidermonkey # SpiderMonkey (js shell) wrapper HTTP service
ββ trace-service # ECMAScript abstract operations tracer (engine262-based)
ββ frontend # Next.js UI (playground, V8 pipeline, abstract ops visualizer)
/packages/engine-runtime # Shared engine HTTP wrapper + the flag catalog
/engines/dockerfiles # Dockerfiles for the engine base images (d8, hermes, jsc, js shell)
/infra/k8s # kustomize base/dev/prod for k3s/Traefik + NetworkPolicies/PDBs
/infra/node # Host-level k3s config applied over SSH, not by kubectl
/docs # Infra map and point-in-time reviews
/scripts # Smoke test + manifest validation helpers
The four engine-* services are thin wrappers around one implementation,
packages/engine-runtime, consumed as a file:
dependency by both them and the api gateway β which is why the flag catalog
cannot drift between the two layers.
The spec-level tracing behind /type-conversion and /equality is powered by a
fork of engine262 (branch
jslab/trace-instrumentation) with trace instrumentation, vendored as a git
submodule at apps/trace-service/engine262.
For a one-page infra diagram (Docker + Kubernetes), see docs/infra.md.
The api and the four engine services bake in packages/engine-runtime, so they
build from the repo root (-f <path>/Dockerfile .). The frontend and the
trace service are self-contained and build from their own directory. All
commands below are run from the repo root:
- Frontend:
docker build -t pavlof01/jslab-frontend apps/frontend - Trace service:
docker build -t pavlof01/jslab-trace-service apps/trace-service(requires the engine262 submodule to be initialized) - API:
docker build -f apps/api/Dockerfile -t pavlof01/jslab-api . - Engine V8:
docker build -f apps/engine-v8/Dockerfile --build-arg V8_BASE_IMAGE=pavlof01/v8-d8:latest -t pavlof01/jslab-engine-v8 . - Engine Hermes:
docker build -f apps/engine-hermes/Dockerfile --build-arg HERMES_BASE_IMAGE=pavlof01/hermes:latest -t pavlof01/jslab-engine-hermes . - Engine JSC:
docker build -f apps/engine-jsc/Dockerfile --build-arg JSC_BASE_IMAGE=pavlof01/jsc:debug -t pavlof01/jslab-engine-jsc . - Engine SpiderMonkey:
docker build -f apps/engine-spidermonkey/Dockerfile --build-arg SPIDERMONKEY_BASE_IMAGE=pavlof01/spidermonkey:debug -t pavlof01/jslab-engine-spidermonkey . - You can swap
pavlof01/v8-d8/pavlof01/hermes/pavlof01/jsc/pavlof01/spidermonkeywith your own base layers that already containd8/hermes/jsc/js.
Each Dockerfile has a dev and a prod target; docker compose and Skaffold
build --target dev, CI builds the default (production) target.
The engine binaries themselves are built by the Dockerfiles in
engines/dockerfiles/: Dockerfile.v8 (d8),
Dockerfile.hermes (hermes/hermesc/hbcdump), Dockerfile.jsc (jsc) and
Dockerfile.spidermonkey (js shell). Prebuilt images are published under the
pavlof01/* Docker Hub namespace
(pavlof01/v8-d8, pavlof01/hermes, pavlof01/jsc,
pavlof01/spidermonkey), so you only need these Dockerfiles when rebuilding an
engine from source.
- Apply base stack:
kubectl apply -k infra/k8s/base - Namespace:
jslab - Ingress (Traefik): routes
/apito theapiservice and/tofrontend(Next.js) with explicit router priorities. - NetworkPolicy: only API reachable from Traefik/namespace, engines reachable only from API, Redis reachable only from API.
- Pods run with
runAsNonRoot,allowPrivilegeEscalation: false,readOnlyRootFilesystem: true,seccompProfile: RuntimeDefault;/tmpmounted fromemptyDir. - PodDisruptionBudgets for api and frontend;
infra/k8s/hpa.todo.yamlholds a ready-to-enable HPA for the API.
For the ingress routing model, the network-policy model and the client-IP trust
settings, see infra/README.md.
- Build images β see Docker images above for the exact commands (note the repo-root build context for the api and the engines).
- Apply manifests:
kubectl apply -k infra/k8s/base
- Check readiness:
kubectl -n jslab get pods,svc,ingress
- Local access (optional):
- Add a host record:
/etc/hostsβ127.0.0.1 jslab.local(or your Ingress/LoadBalancer IP).
- Add a host record:
- Test API:
# Browser/client calls Next.js at /api/run (no auth required). curl -k -H "content-type: application/json" \ -d '{"engine":"v8","sourceText":"1+1"}' \ https://jslab.local/api/run
Short-lived debug pods can hit a brief window where NetworkPolicy rules have not been applied yet, causing transient connection failures (e.g., "Could not connect") for the first few seconds.
Recommended approaches:
- Use a long-lived debug pod and
kubectl execinto it. - Or add a short
sleepbefore curling services.
Example debug pod (long-lived):
kubectl -n jslab run debug-shell \
--rm -it --restart=Never \
--image=curlimages/curl:8.5.0 \
--command -- sleep 3600Verify engine connectivity:
kubectl -n jslab exec -it debug-shell -- \
curl -sS http://engine-v8:8080/healthzVerify API run request:
kubectl -n jslab exec -it debug-shell -- \
curl -sS -H "content-type: application/json" \
-d '{"engine":"v8","sourceText":"1+1"}' \
http://api:8080/api/run-
Start the dev loop (from repo root):
skaffold dev --port-forward -n jslab
-
Access UI and API locally:
- UI:
http://127.0.0.1:3000/ - API health:
curl -sS http://127.0.0.1:8080/healthz
- UI:
-
Apple Silicon / arm64 note:
- If you see
rosetta error: failed to open elf at /lib64/ld-linux-x86-64.so.2, the engine is trying to run anamd64binary inside anarm64container. - Quick dev fix (build engine images as
linux/amd64under emulation):- Stop
skaffold dev(Ctrl+C) - Run:
skaffold dev --port-forward -n jslab --check-cluster-node-platforms=false --cache-artifacts=false
- Stop
- If you still see the old error with
cacheHit: true, flush Redis cache or wait for TTL:kubectl -n jslab run tmp-redis --rm -it --image=redis:7-alpine --restart=Never -- redis-cli -h redis FLUSHALL
- If you see
The gateway's own OpenAPI document is served at GET /api/openapi.json, with a
browsable rendering at /api/docs.
| Endpoint | Purpose |
|---|---|
POST /api/run |
Run a snippet on one engine (see below) |
GET /api/flags |
The per-engine flag catalog, with descriptions and categories |
GET /api/engines |
Each engine key with the version string its binary reports (null when the shell cannot say) |
POST /api/trace/execute/type-conversion |
{ functionName, input, preferredType? } β spec trace |
POST /api/trace/execute/equality |
{ input } (a binary expression such as {} == ![]) β spec trace |
POST /api/keys |
Mint a self-service API key (raises the general and trace quotas) |
DELETE /api/keys |
Revoke the key presented in x-api-key / Authorization: Bearer |
GET /api/openapi.json, /api/docs |
OpenAPI document and API reference UI |
GET /healthz, GET /metrics |
Liveness probe and Prometheus metrics |
- Endpoint:
POST /api/run - Request:
{
"engine": "v8 | hermes | sm | jsc",
"sourceText": "string",
"options": { "flags": ["..."], "timeoutMs": 2000 }
}- Response:
{
"ok": true,
"stdout": "...",
"stderr": "...",
"artifacts": [],
"meta": { "durationMs": 0, "engine": "v8", "cacheHit": false }
}artifactsis part of the contract but the engine services currently return it empty β every engine's output arrives as text onstdout/stderr.metaalso carriesdroppedFlagswhen the allowlist rejected something, andoutputTruncated+outputLimitByteswhen the combined output hit the 2 MB (MAX_OUTPUT_BYTES) cap.- Normalization:
sourceTextis capped atMAX_SOURCE_LENGTH(20 000 chars),timeoutMsis clamped into[MIN_TIMEOUT_MS, MAX_TIMEOUT_MS]=[250, 5000](default 2000), and at mostMAX_FLAGS(10) flags are considered. - Rate limits: Redis counters per client IP, layered per bucket β
general60/min,heavy(engine-spawning) 20/min,trace30/min. A self-service API key raises general and trace to its own quota (240/min), while the heavy bucket stays separately capped (60/min). 429 responses carryRetry-Afterandmeta.retryAfter. - Cache: Redis hash of engine+source+normalized flags+timeout bucket, TTL
CACHE_TTL_SECONDS(default 600s); deterministic failures get the shorterNEGATIVE_CACHE_TTL_SECONDS(default 30s).
curl -X POST https://jslab.local/api/run \
-H "content-type: application/json" \
-d '{"engine":"v8","sourceText":"function f(){return 1+2};f();","options":{"flags":["--print-bytecode"],"timeoutMs":2000}}'curl -sS https://jslab.su/api/run \
-H "content-type: application/json" \
-d '{"engine":"v8","sourceText":"1+2","options":{"flags":["--print-bytecode"]}}'- Engine selector and preset flags β
- Sandbox API
/api/runβ - Execution history and βShare sessionβ links β
- AST tree visualization (
--print-ast) β - Bytecode diff viewer (Myers diff + Shiki) β
- V8 compilation pipeline diagram (Tokens β AST β Ignition β Sparkplug β Maglev β TurboFan β Deopt) β
- ECMAScript abstract operations step-through visualizer β
- Hermes IR viewer β
- Multi-engine playground β
- Embeddable playground / bytecode widgets (
/embed/*, with oEmbed) β - Inline opcode descriptions in the output panel β
- Standalone opcode documentation pages (
/docs/{engine}/{opcode}) - Snippet sharing & voting
- V8 heap and log visualizer (
v8.log) - Flamegraph integration
- WebAssembly comparison layer
- AI Explain Mode for bytecode and optimization traces
This project is licensed under the MIT License β Β© 2026 Alexey Pavlov.
JSLab wraps and builds upon several open-source JavaScript engines and tools, each distributed under its own license:
- V8 β BSD-3-Clause
- JavaScriptCore (part of WebKit) β LGPL-2.1 and BSD-2-Clause
- SpiderMonkey β MPL-2.0
- Hermes β MIT
- engine262 β MIT
Trace output from the abstract-operations visualizer reproduces algorithm text from the ECMA-262 specification, Β© Ecma International.