Skip to content

性能与合规数据 ​

这一页只做一件事:用数字说话。所有数字都有可复现的命令、明确的口径和公开的判定阈值 —— 包括未达标的那一项。

一句话结论

验收项判定依据
解释器算力达 goja 同级❌ 未达标fib28 纯计算 gox ≈ 311ms vs goja ≈ 130ms → 慢 2.4 倍(阈值 ±25%)
UI 无 GC 毛刺✅ 达标重压聚合 p95 帧间隔 5.95ms ≤ 16.7ms;单次 GC 停顿 max 1.382ms ≤ 5ms
test262 language 合规率97.0%v0.15.0,较 0.14.x 的 85.6% 再提 11.4pp
test262 built-ins 合规率62.1%14804 / 23823(较 35.7% +26.4pp);失败面仍集中:502 个簇里 Top 10 占 30.32%

一、测试环境 ​

所有数字在同一台机器、同一天、同一批脚本下测得(脚本零依赖、外部计时)。

项值
机器Apple M2,8 核,8 GB,macOS 26.6.2(arm64)
Gogo1.26.2
Nodev22.22.2
gojav0.0.0-20261001174550-3ccc9c78af18(bench/goja 独立 module)
日期2026-10-02(UTC)

真机侧指标(屏幕密度、触控、帧率、内存 PSS、冷启动)口径不同、不可直接相减, 单独维护在 docs/mobile-perf-baseline.md。

二、解释器算力基线 ​

三个脚本:startup_idle.js(引擎启动 + 最小求值)、fib28.js(fib(28) 递归 + 10 万次函数调用)、timers_10k.js(1 万次 setTimeout(…,0) 全部触发)。 每项外部 wall time 跑 5 次取中位数。

bash
./scripts/bench-all.sh --runs 5
栈启动 (ms)fib28 + 10万次调用 (ms)1万次定时器 (ms)空载 RSS (KB)
gox1132212422592
node (V8)554515248432
goja71371217792

口径诚实性:fib28_ms 是整脚本 wall time(含启动 + 解析 + 求值); goja 内核不提供宿主定时器,其定时器列用一个最小事件循环补齐,不能当作 "goja 调度器性能";RSS 衡量"空壳内存"而非算力。

2.1 扣掉启动开销后的真相 ​

fib28 的 wall time 混着启动 —— node 的启动(55ms)比它的 fib28 总耗时(45ms) 还大,说明那一列几乎全是启动噪声。用 startup_idle 中位数相减:

栈fib28 纯计算估算相对 gox
gox322 − 11 ≈ 311 ms1.00×
goja137 − 7 ≈ 130 ms0.42×(goja 快 2.4 倍)
node / V8≈ 5 ms(hrtime 内联口径)0.016×(V8 快约 62 倍)

诚实陈述:

  • gox 是纯解释器(零 cgo、直接遍历字节码)。它与 V8 的差距里绝大部分是 "解释器 vs JIT"的固有差距,不能都算成实现缺陷。
  • 真正该对标的是 goja(同样纯解释器):gox 慢约 2.4 倍。这是下一轮 inline cache / 快速路径优化的直接靶子。
  • 口径提醒:仓库旧数据里的"落后 V8 ~15 倍"是原始 wall 比值;按纯计算 口径是 ~60 倍量级。两个数都对,引用时必须说清用的是哪个。
  • 亮点在别处:定时器吞吐已与 goja 同级(124ms vs 121ms,差 2%),甚至快于 本机 node(152ms);启动开销小;空载 RSS 7.8MB(goja) / 22.6MB(gox) / 48.4MB(node)。 短板集中在纯计算热循环。

2.2 "同级"的量化定义 ​

gox_fib28_compute ≤ goja_fib28_compute × 1.25     // ±25% 容差

达标 ≤ ×1.25;优良 ≤ ×1.10;未达标 > ×1.25。 当前 311 / 130 ≈ 2.39× > 1.25 → 未达标,需把纯计算提速约 2.4 倍才能落闸。

三、UI 帧率与 GC 毛刺 ​

位置:tools/perf/frame_gc_test.go + 压测脚本 testdata/perf/frame_load.js

bash
go test ./tools/perf/            # 轻量探针 + 重压(默认)
go test ./tools/perf/ -short     # 只跑轻量探针

3.1 测什么 ​

  • 真实渲染:脚本走真实 JS 建树,每"帧"更新若干行信号 → 响应式 effect 标脏 → gfx.Pump 走真实 Layout + 脏区 Draw + 上屏。不建平台窗口(用无窗口 Surface 替身)。
  • 帧间隔:相邻两次上屏的时间差 —— 含 JS 更新 + effect + Layout + Draw + 任何 GC 停顿, 是"用户眼里的掉帧"的直接代理量。
  • GC 停顿:runtime/debug.ReadGCStats,取测量窗口内新增的逐次 STW 停顿。只看尾部。
  • 分配速率:独立 goroutine 每 2ms 采样,避免把 ReadMemStats 开销插进渲染循环。

3.2 实测数据(Apple M2,2026-10-02,GOGC=100) ​

轻量探针(300 行、每帧更新 30 行、600 测量帧):

指标值
帧间隔 p50 / p95 / p990.77 / 1.67 / 1.71 ms
帧间隔 p99.9 / max1.84 / 1.98 ms
GC 次数 / 停顿 max1 / 0.046 ms

重压(800 行、每帧更新 200 行、2400 帧 × 3 次聚合):

指标值
帧间隔 p503.35 ms
帧间隔 p95(判定用)5.95 ms
帧间隔 p999.68 ms
超 16.7ms 帧6 / 2399 = 0.30%
GC 次数(3 次合计)61
GC 停顿 p50 / p95 / p99 / max0.100 / 0.329 / 0.628 / 1.382 ms
GC 停顿总计(3 次合计)10.71 ms(摊到 7200 帧 ≈ 1.5µs/帧)
分配 平均 / 峰值 / 堆峰值86.6 / 102.4 MB/s / 98.4 MB

三次单跑的离散度(说明为什么判定不能只看 p99/max):

单跑p95p99maxGC 停顿 max
#17.4012.42194.591.382 ms
#24.396.74132.530.316 ms
#35.959.68120.720.664 ms

GC 停顿始终亚毫秒~1.4ms,而帧间隔 p99/max 跨运行摆动很大 (p99 6.7~12.4ms、max 120~195ms)—— 差异来自 OS 调度,不是 GC。

3.3 判定阈值 ​

判定为「无 GC 毛刺」当且仅当:
  ① p95 帧间隔 ≤ 16.7 ms    (95% 的帧落在 60fps 单帧预算内)
  ② 单次 GC 停顿 max ≤ 5 ms (软件渲染 + 交互的可感知阈值)
  • 为什么 16.7ms:一帧 16.7ms = 60fps。绝大多数帧不越界,用户就感知不到周期性掉帧。
  • 为什么用 p95 而不是 p99/max:无独占的桌面机上,p99/max 由 OS 调度抖动主导 (同负载 3 次实测 p99 在 7~37ms、max 在 109~367ms 间摆动),拿它判会把"被别的进程 抢了 CPU"误判成"GC 毛刺"。p95 跨运行稳定(4.4~7.4ms)。
  • 为什么 5ms:5ms 级停顿通常不可感知,10ms+ 才会在快速滚动时"顿一下"。 实测 max 仅 1.382ms,离阈值有 ~3.6 倍余量。

当前判定:① 5.95 ≤ 16.7 ✅;② 1.382 ≤ 5 ✅ → 无 GC 毛刺(达标)。

硬闸门 vs 软判定:perf 测试只对"数量级劣化"硬失败(p99 > 200ms、GC 停顿 max > 100ms、 重绘帧数异常),正常抖动不会让 CI 误报;16.7 / 5ms 阈值作为 verdict 写进 JSON 供验收引用。

四、test262 合规率 ​

套件合规率通过 / 总数
language97.0%v0.15.0(逐用例明细见当批 artifact)
built-ins62.1%14804 / 23823

0.15.0 这一轮的提分

language 从 85.6% 提到 97.0%(+11.4pp)、built-ins 从 35.7% 提到 62.1%(+6301 例,+26.4pp),全程逐用例比对零真实退潮。最大的一块来自 TypedArray/prototype:35.7% 那版它的基线是 0.00%(整段没做),这一轮补成 成块实现后已退出 Top 10。

4.1 失败面仍然集中 ​

built-ins 的失败不是"九千条零散 bug",而是 502 个簇里的 10 个就占 失败面的 30.32%。所以排期按簇开单,而不是按条修。

先分形态(互斥,各形态之和 = 失败总数 9019):

形态规模占失败面占全量排期含义
语义偏差596666.15%25.04%能力在,值/顺序/边界/this 校验与规范不符 —— 主战场,逐方法对齐
功能零实现277330.75%11.64%停在编译前端或运行期报"没有这个东西" —— 成块补实现
harness 依赖1912.12%0.80%用例根本没进引擎 —— runner 侧,成本最低,先清
crash690.77%0.29%超时挂死 —— 优先级最高,会污染整个分片
假阳性退潮200.22%0.08%negative 用例判定失配(全部落在 RegExp/property-escapes)

一句话读法:三分之二是"做得不对",三成是"没做" —— 与 35.7% 那版相比 (语义偏差 63.28% / 功能零实现 33.84%),主战场仍是语义对齐。

Top 10 簇(族 = 用例目录前 3 段):

#目录族失败形态簇规模族基线(通过/总数)
1built-ins/String/prototype语义偏差501502/1073 = 46.78%
2built-ins/RegExp/property-escapes功能零实现447143/613 = 23.33%
3built-ins/Iterator/prototype功能零实现295103/513 = 20.08%
4built-ins/Object/create语义偏差29425/320 = 7.81%
5built-ins/Array/prototype语义偏差2712486/2812 = 88.41%
6built-ins/DataView/prototype语义偏差233132/499 = 26.45%
7built-ins/Object/defineProperty语义偏差198888/1131 = 78.51%
8built-ins/Set/prototype语义偏差181162/357 = 45.38%
9built-ins/RegExp/prototype语义偏差158212/487 = 43.53%
10built-ins/Object/defineProperties语义偏差157466/632 = 73.73%

Temporal 已退出 Top 10 —— 35.7% 那版它一族占了 6 个簇,这一轮整族补上; Array/prototype 也从 34.39% 提到 88.41%。剩下的洼地是 Object/create(7.81%, 属性描述符语义)与 RegExp/property-escapes(23.33%,Unicode 属性转义需要 UCD 数据表)。

跑法:gox test262 -suite built-ins -jobs 16(128 固定分片,约 100s 跑完 23823 例)。 口径:分母是"实际收集到的用例",runner 侧跳过的 harness 依赖计入分母不计入通过。 分片数固定 128 且与 -jobs 解耦 —— 引擎有包级可变状态,分片数若跟着 -jobs 变, 通过数会随并发度翻转。

五、第三方 npm 库兼容性 ​

实测 8 个主流库,只有 uuid 部分可用。测试方法:npm 安装 → 写最小 ESM 冒烟脚本 覆盖核心 API → gox <script>.mjs 执行 → 记录通过/失败及第一失败点。

库版本结论失败原因
uuid14.0.3部分可用v4() 生成、唯一性通过;validate() 报 REGEX is not defined(正则字面量子集不支持)
lodash-es4.18.1不可用跨模块具名导入绑定失败:toInteger is not defined(模块间循环互引时导入绑定未正确建立)
axios1.20.0不可用压缩产物含字符类 [\s\uFEFF\xA0],编译期 Invalid regular expression
dayjs1.11.23不可用默认入口是 UMD/CJS(红线一,不做 CJS);改 dayjs/esm 后可解析,但模块作用域重复声明 diff 被拒绝
validator13.15.35不可用编译期 missing semicolon before COMMA(压缩产物含子集外写法)
qs6.16.0不可用主入口 CJS:require is not defined(红线一,刻意 loud failure)
ms2.1.3不可用主入口 CJS:module is not defined(红线一)
nanoid6.0.2不可用模块绑定失败:GET_RANDOM_LIMIT is not defined(同 lodash-es 一类)

5.1 失败模式归类(比单库结论更重要) ​

实测失败全部落在 4 类根因上:

  1. 纯 CJS 包(qs、ms、dayjs 默认入口、lodash 主包):红线一既定行为, require/module is not defined,loud failure,非 bug。
  2. 模块绑定在循环/深层互引下失效(lodash-es、nanoid):import 的具名绑定在被依赖 模块执行时未建立,报 X is not defined。这是运行时模块求值顺序的真实缺陷, 堵死了内部多文件互引的大包 —— 优先级最高的生态欠账。
  3. 正则子集(axios、lodash-es、uuid 部分):lookahead (?=…)、\uFEFF/\u200d 等 unicode 转义不支持。现代打包产物的压缩代码普遍踩中。
  4. 压缩产物语法子集缺口(validator、dayjs.min):parser 在合法但压缩过的语法上报 missing semicolon before COMMA。

选包建议

单文件、无内部循环互引、不依赖复杂正则的纯 ESM 小包可以跑(uuid 主体即属此类); 其余主流库 v0 基本不可用。优先用 Gox 内置模块(fs / path / http / process) 或 gx/* 宿主模块替代。

六、已有的性能机制 ​

这些是已经存在的机制(本页只盘点、不改),也是"为什么 gox 在 UI 侧不掉链子"的答案:

机制实现说明
gx/dev 帧埋点gfx/dev.go 的 devFrameTick只做单调计数,不测 layout/draw 耗时(字段形状被 TestDevSnapshotShape 锁死)
image LRU 缓存gfx/image.gopath → 解码后 *image.RGBA,按张数上限 16 淘汰
glyph LRU 缓存gfx/font.go(size, rune) → 字形掩码,容量 1024
<scroll vlist> 虚拟化gfx/vlist.go只物化视口 + 上下 buffer(缺省 2 行)

<scroll vlist> 实测(10 万行,Windows / i7 机器 —— 另一台机器,不与本文数字相减):

行数首帧 全量首帧 vlist滚动帧 全量滚动帧 vlist
10 万2187 ms89 ms998.8 ms0.115 ms

首帧与行数脱钩(vlist 恒 64 次渲染调用),滚动帧与行数完全脱钩。

判定纪律:必须数"渲染调用次数",不能数树上剩多少节点 —— 否则"先全建再销毁"的 假虚拟化会骗过断言。

这是 GUI 渲染路径的成果,不是解释器算力。不要拿它去回答"fib28 慢"。


数据来源:docs/performance.md、 docs/test262-builtins-clusters.md、 docs/compat-matrix.md。

下一站:限制与常见误区 看逐项缺口,或回到 使用教程 开始写代码。

Apache License 2.0 · 用 ❤ 和 Go 编写