Skip to content

Performance and Compliance Data ​

This page does exactly one thing: let the numbers talk. Every figure comes with a reproducible command, an explicit methodology and a published threshold — including the one we miss.

The one-line summary

Acceptance itemVerdictBasis
Interpreter on par with goja❌ Missedfib28 pure compute gox ≈ 311ms vs goja ≈ 130ms → 2.4× slower (±25% threshold)
UI free of GC pauses✅ PassedStressed-run aggregate p95 frame interval 5.95ms ≤ 16.7ms; single GC pause max 1.382ms ≤ 5ms
test262 language compliance97.0%v0.15.0 — up another 11.4pp from 85.6% in 0.14.x
test262 built-ins compliance62.1%14804 / 23823 (up 26.4pp from 35.7%); failures are still concentrated: the top 10 of 502 clusters cover 30.32%

1. Test environment ​

All figures were measured on the same machine, on the same day, with the same scripts (zero dependencies, external timing).

ItemValue
MachineApple M2, 8 cores, 8 GB, macOS 26.6.2 (arm64)
Gogo1.26.2
Nodev22.22.2
gojav0.0.0-20261001174550-3ccc9c78af18 (standalone bench/goja module)
Date2026-10-02 (UTC)

On-device metrics (screen density, touch, frame rate, PSS memory, cold start) use a different methodology and must not be subtracted from these; they live separately in docs/mobile-perf-baseline.md.

2. Interpreter throughput baseline ​

Three scripts: startup_idle.js (engine start + minimal evaluation), fib28.js (fib(28) recursion + 100k function calls) and timers_10k.js (10k setTimeout(…,0) all fired). Each external wall time is the median of 5 runs.

bash
./scripts/bench-all.sh --runs 5
StackStartup (ms)fib28 + 100k calls (ms)10k timers (ms)Idle RSS (KB)
gox1132212422592
node (V8)554515248432
goja71371217792

Honest caveats: fib28_ms is whole-script wall time (startup + parse + evaluation); goja's kernel provides no host timers, so its timer column uses a minimal event loop and cannot be read as "goja's scheduler performance"; RSS measures the "empty shell", not throughput.

2.1 The truth after subtracting startup ​

fib28 wall time is polluted by startup — node's startup (55ms) is larger than its total fib28 time (45ms), so that column is almost pure startup noise. Subtracting the startup_idle median:

StackEstimated fib28 pure computeRelative to gox
gox322 − 11 ≈ 311 ms1.00×
goja137 − 7 ≈ 130 ms0.42× (goja is 2.4× faster)
node / V8≈ 5 ms (hrtime inline)0.016× (V8 is ~62× faster)

Stated honestly:

  • gox is a pure interpreter (zero cgo, walking bytecode directly). Most of its gap to V8 is the inherent interpreter-vs-JIT gap and shouldn't all be blamed on the implementation.
  • The fair comparison target is goja (also a pure interpreter): gox is about 2.4× slower. That is the direct target for the next round of inline-cache / fast-path work.
  • Methodology warning: the older "~15× behind V8" figure is the raw wall ratio; on a pure-compute basis the gap is ~60×. Both are correct — you must state which one you're quoting.
  • Where gox does well: timer throughput is already on par with goja (124ms vs 121ms, 2% apart), faster even than node on this machine (152ms); startup is cheap; idle RSS is 7.8MB (goja) / 22.6MB (gox) / 48.4MB (node). The weak spot is the pure-compute hot loop.

2.2 The quantified definition of "on par" ​

gox_fib28_compute ≤ goja_fib28_compute × 1.25     // ±25% tolerance

Pass ≤ ×1.25; excellent ≤ ×1.10; fail > ×1.25. Currently 311 / 130 ≈ 2.39× > 1.25 → missed; roughly a 2.4× pure-compute speedup is needed to clear the gate.

3. UI frame rate and GC pauses ​

Location: tools/perf/frame_gc_test.go + load script testdata/perf/frame_load.js

bash
go test ./tools/perf/            # light probe + stress (default)
go test ./tools/perf/ -short     # light probe only

3.1 What is measured ​

  • Real rendering: the script builds the tree through real JS; each "frame" updates a number of row signals → reactive effects mark dirty → gfx.Pump runs the real Layout + dirty-rect Draw + present. No platform window is created (a windowless Surface stand-in is used).
  • Frame interval: the time between consecutive presents — it includes the JS update + effects + Layout + Draw + any GC pause, making it a direct proxy for the frame drops a user would perceive.
  • GC pauses: runtime/debug.ReadGCStats, taking the newly added per-cycle STW pauses in the measurement window. Tail only.
  • Allocation rate: sampled every 2ms by a separate goroutine so ReadMemStats overhead never lands inside the render loop.

3.2 Measured data (Apple M2, 2026-10-02, GOGC=100) ​

Light probe (300 rows, 30 rows updated per frame, 600 measured frames):

MetricValue
Frame interval p50 / p95 / p990.77 / 1.67 / 1.71 ms
Frame interval p99.9 / max1.84 / 1.98 ms
GC count / pause max1 / 0.046 ms

Stress (800 rows, 200 rows updated per frame, 2400 frames × 3 runs aggregated):

MetricValue
Frame interval p503.35 ms
Frame interval p95 (the verdict metric)5.95 ms
Frame interval p999.68 ms
Frames over 16.7ms6 / 2399 = 0.30%
GC count (3 runs combined)61
GC pause p50 / p95 / p99 / max0.100 / 0.329 / 0.628 / 1.382 ms
GC pause total (3 runs combined)10.71 ms (≈1.5µs/frame over 7200 frames)
Allocation avg / peak / heap peak86.6 / 102.4 MB/s / 98.4 MB

Spread across the three individual runs (this shows why the verdict can't rest on p99/max):

Runp95p99maxGC pause max
#17.4012.42194.591.382 ms
#24.396.74132.530.316 ms
#35.959.68120.720.664 ms

GC pauses stay sub-millisecond to 1.4ms, while frame-interval p99/max swing widely across runs (p99 6.7–12.4ms, max 120–195ms) — that spread comes from OS scheduling, not GC.

3.3 The thresholds ​

Verdict of "no GC pauses" holds iff:
  ① p95 frame interval ≤ 16.7 ms    (95% of frames inside the 60fps single-frame budget)
  ② single GC pause max ≤ 5 ms      (the perceptibility threshold for software rendering + interaction)
  • Why 16.7ms: one frame at 60fps. If nearly all frames stay inside it, users perceive no periodic drops.
  • Why p95 and not p99/max: on a non-exclusive desktop machine, p99/max are dominated by OS scheduling jitter (across three runs of the same load, p99 varied 7–37ms and max 109–367ms). Judging on those would mistake "another process stole the CPU" for "a GC pause". p95 is stable across runs (4.4–7.4ms).
  • Why 5ms: 5ms-class pauses are generally imperceptible; 10ms+ only shows up as a hitch during fast scrolling. The measured max of 1.382ms leaves ~3.6× headroom.

Current verdict: ① 5.95 ≤ 16.7 ✅; ② 1.382 ≤ 5 ✅ → no GC pauses (passed).

Hard gate vs soft verdict: the perf test only fails hard on order-of-magnitude regression (p99 > 200ms, GC pause max > 100ms, anomalous redraw counts), so ordinary jitter never produces a false CI failure; the 16.7 / 5ms thresholds are written into the JSON as verdict for acceptance references.

4. test262 compliance ​

SuiteCompliancePassed / total
language97.0%v0.15.0
built-ins62.1%14804 / 23823

What 0.15.0 moved

language went from 85.6% to 97.0%; built-ins went from 35.7% to 62.1% (+6301 tests). The single biggest block was TypedArray/prototype: at 35.7% its baseline was 0.00% (the whole segment unimplemented); it is now implemented in blocks and has dropped out of the top 10.

4.1 The failure surface is still concentrated ​

built-ins failures are not "nine thousand scattered bugs" — the top 10 of 502 clusters alone cover 30.32% of the failure surface. So work is scheduled by cluster, not by individual test.

Breakdown by mode (mutually exclusive; the modes sum to the 9019 failures):

ModeSizeShare of failuresShare of suiteScheduling implication
Semantic deviation596666.15%25.04%Capability exists but values/order/boundaries/this-checks diverge — the main battlefield, align method by method
Feature absent277330.75%11.64%Stops in the front end or errors "no such thing" at runtime — implement in blocks
harness dependency1912.12%0.80%The test never reached the engine — runner side, cheapest, clear first
crash690.77%0.29%Hangs on timeout — highest priority, it poisons an entire shard
False-positive retreat200.22%0.08%negative-test verdict mismatch (all in RegExp/property-escapes)

In one line: two thirds are "done wrong", three tenths are "not done" — against the 35.7% snapshot (63.28% / 33.84%), the main battlefield is still semantic alignment.

Top 10 clusters (family = first 3 path segments of a test):

#Directory familyFailure modeCluster sizeFamily baseline (pass/total)
1built-ins/String/prototypeSemantic deviation501502/1073 = 46.78%
2built-ins/RegExp/property-escapesFeature absent447143/613 = 23.33%
3built-ins/Iterator/prototypeFeature absent295103/513 = 20.08%
4built-ins/Object/createSemantic deviation29425/320 = 7.81%
5built-ins/Array/prototypeSemantic deviation2712486/2812 = 88.41%
6built-ins/DataView/prototypeSemantic deviation233132/499 = 26.45%
7built-ins/Object/definePropertySemantic deviation198888/1131 = 78.51%
8built-ins/Set/prototypeSemantic deviation181162/357 = 45.38%
9built-ins/RegExp/prototypeSemantic deviation158212/487 = 43.53%
10built-ins/Object/definePropertiesSemantic deviation157466/632 = 73.73%

Temporal has dropped out of the top 10 — at 35.7% it held 6 of the 10 slots as a single family; this round filled the whole family in. Array/prototype also moved from 34.39% to 88.41%. The remaining holes are Object/create (7.81%, property-descriptor semantics) and RegExp/property-escapes (23.33%, Unicode property escapes need the UCD tables).

How to run: gox test262 -suite built-ins -jobs 16 (128 fixed shards; ~100s for 23823 tests). Methodology: the denominator is the tests actually collected; harness dependencies skipped by the runner count toward the denominator but not toward passes. The shard count is fixed at 128 and decoupled from -jobs — the engine has package-level mutable state, so if the shard count tracked -jobs, the pass count would flip with concurrency.

5. Third-party npm compatibility ​

Eight mainstream libraries were tested; only uuid is partially usable. Method: npm install → write a minimal ESM smoke script covering the core API → run gox <script>.mjs → record pass/fail and the first failure point.

LibraryVersionVerdictFailure reason
uuid14.0.3Partially usablev4() generation and uniqueness pass; validate() reports REGEX is not defined (regex literal subset unsupported)
lodash-es4.18.1UnusableCross-module named import binding fails: toInteger is not defined (import bindings aren't properly established under circular references between modules)
axios1.20.0UnusableMinified output contains the character class [\s\uFEFF\xA0]; compile-time Invalid regular expression
dayjs1.11.23UnusableDefault entry is UMD/CJS (red line one, no CJS); after switching to dayjs/esm it parses but compilation fails on a duplicate diff declaration in module scope
validator13.15.35UnusableCompile-time missing semicolon before COMMA (minified output contains syntax outside the subset)
qs6.16.0UnusableCJS main entry: require is not defined (red line one, deliberately a loud failure)
ms2.1.3UnusableCJS main entry: module is not defined (red line one)
nanoid6.0.2UnusableModule binding failure: GET_RANDOM_LIMIT is not defined (same class as lodash-es)

5.1 Root-cause grouping (more important than the per-library verdicts) ​

All measured failures fall into four root causes:

  1. Pure CJS packages (qs, ms, dayjs default entry, lodash main package): established behaviour under red line one — require/module is not defined, a loud failure, not a bug.
  2. Module bindings failing under circular/deep cross-references (lodash-es, nanoid): the import named binding isn't established when the depended-on module executes, giving X is not defined. This is a real defect in runtime module evaluation order and it blocks large packages with internal multi-file cross-references — the highest-priority ecosystem debt.
  3. Regex subset (axios, lodash-es, parts of uuid): lookahead (?=…) and unicode escapes such as \uFEFF / \u200d are unsupported. Modern minified bundles hit this routinely.
  4. Minified-output syntax subset gaps (validator, dayjs.min): the parser reports missing semicolon before COMMA on valid but minified syntax.

Which packages to pick

Single-file, no internal circular references and no complex regex pure-ESM packages will run (uuid's core is in this class); the rest of the mainstream ecosystem is essentially unusable at v0. Prefer Gox's built-in modules (fs / path / http / process) or the gx/* host modules.

6. Existing performance machinery ​

These mechanisms already exist (this page only takes inventory); they are the answer to "why doesn't gox stumble on the UI side":

MechanismImplementationNotes
gx/dev frame instrumentationdevFrameTick in gfx/dev.goMonotonic counting only, no layout/draw timing (the field shape is locked by TestDevSnapshotShape)
image LRU cachegfx/image.gopath → decoded *image.RGBA, evicted at a count cap of 16
glyph LRU cachegfx/font.go(size, rune) → glyph mask, capacity 1024
<scroll vlist> virtualisationgfx/vlist.goMaterialises only the viewport + above/below buffer (2 rows by default)

<scroll vlist> measurement (100k rows, Windows / i7 machine — a different machine, not to be subtracted from the figures above):

RowsFirst frame, fullFirst frame, vlistScroll frame, fullScroll frame, vlist
100k2187 ms89 ms998.8 ms0.115 ms

The first frame is decoupled from row count (vlist always issues 64 render calls), and scroll frames are fully decoupled from row count.

Verification discipline: you must count "render calls", not "nodes left in the tree" — otherwise a fake virtualisation that builds everything then destroys it will fool the assertion.

This is a GUI rendering path result, not interpreter throughput. Don't use it to answer "why is fib28 slow".


Sources: docs/performance.md, docs/test262-builtins-clusters.md, docs/compat-matrix.md.

Next stop: Limits and Common Pitfalls for the itemised gaps, or back to the Tutorials to start writing code.