性能与合规数据
这一页只做一件事:用数字说话。所有数字都有可复现的命令、明确的口径和公开的判定阈值 —— 包括未达标的那一项。
一句话结论
| 验收项 | 判定 | 依据 |
|---|---|---|
| 解释器算力达 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) |
| Go | go1.26.2 |
| Node | v22.22.2 |
| goja | v0.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 次取中位数。
./scripts/bench-all.sh --runs 5| 栈 | 启动 (ms) | fib28 + 10万次调用 (ms) | 1万次定时器 (ms) | 空载 RSS (KB) |
|---|---|---|---|---|
| gox | 11 | 322 | 124 | 22592 |
| node (V8) | 55 | 45 | 152 | 48432 |
| goja | 7 | 137 | 121 | 7792 |
口径诚实性:fib28_ms 是整脚本 wall time(含启动 + 解析 + 求值); goja 内核不提供宿主定时器,其定时器列用一个最小事件循环补齐,不能当作 "goja 调度器性能";RSS 衡量"空壳内存"而非算力。
2.1 扣掉启动开销后的真相
fib28 的 wall time 混着启动 —— node 的启动(55ms)比它的 fib28 总耗时(45ms) 还大,说明那一列几乎全是启动噪声。用 startup_idle 中位数相减:
| 栈 | fib28 纯计算估算 | 相对 gox |
|---|---|---|
| gox | 322 − 11 ≈ 311 ms | 1.00× |
| goja | 137 − 7 ≈ 130 ms | 0.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
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 / p99 | 0.77 / 1.67 / 1.71 ms |
| 帧间隔 p99.9 / max | 1.84 / 1.98 ms |
| GC 次数 / 停顿 max | 1 / 0.046 ms |
重压(800 行、每帧更新 200 行、2400 帧 × 3 次聚合):
| 指标 | 值 |
|---|---|
| 帧间隔 p50 | 3.35 ms |
| 帧间隔 p95(判定用) | 5.95 ms |
| 帧间隔 p99 | 9.68 ms |
| 超 16.7ms 帧 | 6 / 2399 = 0.30% |
| GC 次数(3 次合计) | 61 |
| GC 停顿 p50 / p95 / p99 / max | 0.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):
| 单跑 | p95 | p99 | max | GC 停顿 max |
|---|---|---|---|---|
| #1 | 7.40 | 12.42 | 194.59 | 1.382 ms |
| #2 | 4.39 | 6.74 | 132.53 | 0.316 ms |
| #3 | 5.95 | 9.68 | 120.72 | 0.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 合规率
| 套件 | 合规率 | 通过 / 总数 |
|---|---|---|
language | 97.0% | v0.15.0(逐用例明细见当批 artifact) |
built-ins | 62.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):
| 形态 | 规模 | 占失败面 | 占全量 | 排期含义 |
|---|---|---|---|---|
| 语义偏差 | 5966 | 66.15% | 25.04% | 能力在,值/顺序/边界/this 校验与规范不符 —— 主战场,逐方法对齐 |
| 功能零实现 | 2773 | 30.75% | 11.64% | 停在编译前端或运行期报"没有这个东西" —— 成块补实现 |
| harness 依赖 | 191 | 2.12% | 0.80% | 用例根本没进引擎 —— runner 侧,成本最低,先清 |
| crash | 69 | 0.77% | 0.29% | 超时挂死 —— 优先级最高,会污染整个分片 |
| 假阳性退潮 | 20 | 0.22% | 0.08% | negative 用例判定失配(全部落在 RegExp/property-escapes) |
一句话读法:三分之二是"做得不对",三成是"没做" —— 与 35.7% 那版相比 (语义偏差 63.28% / 功能零实现 33.84%),主战场仍是语义对齐。
Top 10 簇(族 = 用例目录前 3 段):
| # | 目录族 | 失败形态 | 簇规模 | 族基线(通过/总数) |
|---|---|---|---|---|
| 1 | built-ins/String/prototype | 语义偏差 | 501 | 502/1073 = 46.78% |
| 2 | built-ins/RegExp/property-escapes | 功能零实现 | 447 | 143/613 = 23.33% |
| 3 | built-ins/Iterator/prototype | 功能零实现 | 295 | 103/513 = 20.08% |
| 4 | built-ins/Object/create | 语义偏差 | 294 | 25/320 = 7.81% |
| 5 | built-ins/Array/prototype | 语义偏差 | 271 | 2486/2812 = 88.41% |
| 6 | built-ins/DataView/prototype | 语义偏差 | 233 | 132/499 = 26.45% |
| 7 | built-ins/Object/defineProperty | 语义偏差 | 198 | 888/1131 = 78.51% |
| 8 | built-ins/Set/prototype | 语义偏差 | 181 | 162/357 = 45.38% |
| 9 | built-ins/RegExp/prototype | 语义偏差 | 158 | 212/487 = 43.53% |
| 10 | built-ins/Object/defineProperties | 语义偏差 | 157 | 466/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 执行 → 记录通过/失败及第一失败点。
| 库 | 版本 | 结论 | 失败原因 |
|---|---|---|---|
| uuid | 14.0.3 | 部分可用 | v4() 生成、唯一性通过;validate() 报 REGEX is not defined(正则字面量子集不支持) |
| lodash-es | 4.18.1 | 不可用 | 跨模块具名导入绑定失败:toInteger is not defined(模块间循环互引时导入绑定未正确建立) |
| axios | 1.20.0 | 不可用 | 压缩产物含字符类 [\s\uFEFF\xA0],编译期 Invalid regular expression |
| dayjs | 1.11.23 | 不可用 | 默认入口是 UMD/CJS(红线一,不做 CJS);改 dayjs/esm 后可解析,但模块作用域重复声明 diff 被拒绝 |
| validator | 13.15.35 | 不可用 | 编译期 missing semicolon before COMMA(压缩产物含子集外写法) |
| qs | 6.16.0 | 不可用 | 主入口 CJS:require is not defined(红线一,刻意 loud failure) |
| ms | 2.1.3 | 不可用 | 主入口 CJS:module is not defined(红线一) |
| nanoid | 6.0.2 | 不可用 | 模块绑定失败:GET_RANDOM_LIMIT is not defined(同 lodash-es 一类) |
5.1 失败模式归类(比单库结论更重要)
实测失败全部落在 4 类根因上:
- 纯 CJS 包(qs、ms、dayjs 默认入口、lodash 主包):红线一既定行为,
require/module is not defined,loud failure,非 bug。 - 模块绑定在循环/深层互引下失效(lodash-es、nanoid):
import的具名绑定在被依赖 模块执行时未建立,报X is not defined。这是运行时模块求值顺序的真实缺陷, 堵死了内部多文件互引的大包 —— 优先级最高的生态欠账。 - 正则子集(axios、lodash-es、uuid 部分):lookahead
(?=…)、\uFEFF/\u200d等 unicode 转义不支持。现代打包产物的压缩代码普遍踩中。 - 压缩产物语法子集缺口(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.go | path → 解码后 *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 ms | 89 ms | 998.8 ms | 0.115 ms |
首帧与行数脱钩(vlist 恒 64 次渲染调用),滚动帧与行数完全脱钩。
判定纪律:必须数"渲染调用次数",不能数树上剩多少节点 —— 否则"先全建再销毁"的 假虚拟化会骗过断言。
这是 GUI 渲染路径的成果,不是解释器算力。不要拿它去回答"fib28 慢"。
数据来源:docs/performance.md、 docs/test262-builtins-clusters.md、 docs/compat-matrix.md。