编译到 JavaScript / Wasm:六大技术路线系统比较与统一中层 SSA IR 架构设计
深度研究报告 · 2025年4月 · 覆盖语言:ReScript、Kotlin/JS、Scala.js、Fable、MoonBit、Haxe、Nim、GopherJS、ClojureScript、Gleam
一、问题框架:为什么需要系统性比较
"编译到 JavaScript" 这个标签掩盖了底层极其异质的技术决策空间。当一个语言决定将 JS 作为编译目标时,它实际上在五个维度上做出了相互耦合的设计选择:
- 语义保留策略:源语言语义中有多少必须在 JS 运行时保留,有多少可以在编译期擦除
- 互操作模型:与 JS 生态的边界是清晰类型化的接口,还是逃逸舱口式的动态逃逸
- 中间表示层次:是直接 AST→JS,还是经过一个或多个 IR 降级阶段
- 优化空间:dead code elimination、inlining、constant folding 等优化在哪一层发生
- Wasm 双轨:JS 目标与 Wasm 目标是同一个中层 IR 的两个后端,还是两条完全独立的管道
这五个维度的选择,决定了生成代码的可读性、体积、性能以及 debug 体验。本报告将十种语言归纳为六条技术路线,逐一解剖其架构,最后提出一个面向 JS/Wasm 双目标的统一中层 SSA IR 设计方案。
二、六大技术路线
路线 A:ML系 / OCaml血统(ReScript · Fable · MoonBit)
这三个语言有一个共同的血统节点:它们的创始人都深度参与过 OCaml 编译器生态。
ReScript
ReScript 12(2025年底发布)的编译管道如下:
.res 源文件
→ 解析(ReScript 自有 Parser,纯 OCaml 实现)
→ 类型检查(Hindley-Milner,nominal 类型系统,sound)
→ 内部 AST 清理(v12 移除 OCaml 遗留节点)
→ Lambda IR(类 OCaml Lambda 层,做常量折叠、模式匹配编译)
→ JS 代码生成(直接从 Lambda 到 ES module)
关键设计选择:
- 文件级并行编译:每个
.res 文件独立编译,产出 .cmi(接口)、.cmt(类型树)、.cmj(优化元数据)、.mjs(JS 输出)四个独立制品。没有全程序分析阶段,无需链接器。
- 依赖图由 Rewatch 驱动:v12 的新构建系统彻底取代了原本基于 Ninja 的包装器,采用精确哈希追踪依赖变更,支持 monorepo 包边界并行。
- 类型完全擦除:生成的 JS 不携带任何运行时类型信息。
option<t> 在 JS 层面直接是 null | t,无包装。
- 无运行时库依赖:
@rescript/runtime 是一个极轻量的辅助包,核心逻辑全部在编译期内联。
ReScript 不通过中间 SSA,而是在 Lambda IR 层做有限优化后直接生成 JS。这是速度的来源,也是整程序优化的天花板。
Fable(F# → JS / Python / Rust / Dart)
Fable 的架构依托 FSharp Compiler Services(FCS):
.fs 源文件
→ FCS 前端(类型检查、FIR/TAST 生成)
→ Fable AST(跨目标的中间表示,携带 F# 语义注解)
→ 目标特定后端(JS / Python / Rust / Dart / Erlang)
→ 目标语言代码
Fable 的核心创新是 Fable AST:这是一个比 FCS 的 TAST 更低层但比目标语言更高层的表示,在这一层做 F# 特有的语义转换——currying 展开、Union Type 编译策略(tagged object vs. plain value)、Option<T> 擦除。
Fable 特别强调 可读性:生成的 JS 是符合 ES2015 规范的惯用代码,开发者可以 debug,可以用任何 bundler 处理。代价是某些 F# 语义(如 structural equality)需要运行时辅助函数,引入少量运行时开销。
MoonBit
MoonBit(2024年底开源 Wasm 后端)是这三者中唯一一个以 Wasm 为第一公民设计的语言:
.mbt 源文件
→ MoonBit 类型检查器(容错型,IDE/编译器共享代码库)
→ MoonBit IR(内部 SSA-like IR,类型完全已知)
→ 目标后端:
wasm-gc → WasmGC 二进制(默认,最优化)
js → JavaScript(主要用于工具链和小型脚本)
native → 原生二进制(通过 C 中间层)
MoonBit 的设计哲学是:WasmGC 的类型系统(struct、array、GC-managed heap objects)与 MoonBit 的值类型系统高度同构,因此可以做到几乎零开销的语义映射。生成的 Wasm 二进制极小——这是 Hongbo Zhang(前 OCaml 核心贡献者、ReScript 创始人)的核心设计目标。
路线 B:JVM系 / 多后端 IR(Kotlin/JS · Scala.js)
这两个语言都来自 JVM 生态,但采用了截然不同的 IR 策略。
Kotlin/JS + Kotlin/Wasm
Kotlin 编译器有 K1/K2 两个前端和四个后端(JVM、JS、Native、Wasm)。核心是共享的 Kotlin IR:
Kotlin 源代码
→ K2 前端(FIR: Frontend Intermediate Representation)
→ Fir2Ir(FIR 到 Kotlin IR 的转换)
→ Kotlin IR(树状 IR,携带完整类型信息)
→ 后端特定 lowering:
JS 后端 → TypeScript 可选输出 / ES module JS
Wasm 后端 → WasmGC 二进制(利用 WasmGC 的 struct/array 表达 Kotlin 对象)
Kotlin IR 是真正的"一个 IR,多个后端"设计。JS 后端与 Wasm 后端共享相同的 lowering 基础设施(lazy 属性初始化、inline class 展开、协程状态机等),然后各自分叉做目标特定的代码生成。
Kotlin/Wasm 的关键突破是 2024 年 12 月 Safari 完成对 WasmGC 的支持,使 Kotlin/Wasm 应用能在所有主流浏览器运行。JetBrains 基准测试显示 Kotlin/Wasm 在 UI 密集型场景比 Kotlin/JS 快约 3 倍。
Kotlin/JS 的历史包袱是体积问题:stdlib 全量打包进去,一个 "Hello World" 的 JS 产出可能比等效 TypeScript 大 10 倍以上。这是携带整个 Kotlin 标准库 JS 实现的代价。
Scala.js + Scala.js Wasm Backend
Scala.js 的架构基于一个精心设计的专有 IR——SJSIR(Scala.js IR):
Scala 源代码
→ Scala 编译器前端(类型检查、desugaring)
→ SJSIR 生成(携带 Scala 语义的中间表示)
→ Linker(全程序分析、dead code elimination、优化)
→ 目标后端:
JS 后端 → 优化的 ES module JavaScript
Wasm 后端 → WasmGC 二进制(1.17.0,2024年9月实验性发布)
SJSIR 的设计极为务实:它是 Scala 对 JS 语义的精确建模,而非通用的低层 IR。SJSIR 明确包含:
- 类层次结构(Class、TraitImpl、ModuleClass)
- 显式 JS interop 节点(JSObjectConstr、JSMethodApply 等)
- 精确的可空性标注(v1.17 引入
nullable: Boolean flag)
Wasm 后端的挑战在于:SJSIR 包含大量 JS 互操作语义(@JSExport、字符串拼接中的隐式 toString 等),这些在 Wasm 沙箱中必须通过 JS 胶水层实现。Scala.js 1.19(2025年)已将 Wasm 后端的性能提升到在计算密集型场景超越 JS 后端。
路线 C:多目标平台系(Haxe)
Haxe(2005年,OCaml 实现)是 compile-to-multiple-targets 的先驱:
Haxe 源代码
→ Haxe 编译器前端(类型推断、宏展开)
→ Haxe AST(携带跨目标类型注解)
→ Dead Code Elimination(全程序 DCE)
→ 目标特定代码生成:
JavaScript / TypeScript
C++ / Java / C# / Python / Lua / PHP
HashLink 字节码 / NekoVM 字节码
SWF(Flash)
Haxe 的核心是 externs 系统:用 Haxe 类型注解描述外部平台 API,实现跨目标的类型安全 FFI。生成的 JS 代码极为干净(基准测试中常接近手写 JS 速度),因为 Haxe 在 AST 层面做了大量 inlining 和常量折叠。
Haxe 没有明确的 SSA IR:优化发生在类型化 AST 层面。这使其在单一代码库多目标部署场景(游戏引擎、跨端框架)中有独特优势,但也意味着深度数据流优化较难实现。
路线 D:原生语言→JS 转译(Nim · GopherJS)
Nim
Nim 的 JS 目标是其多后端(C / C++ / ObjC / JS)之一:
Nim 源代码
→ Nim 语义分析(类型系统、宏展开,compile-time 执行)
→ Nim IR(内部图 IR,偏向过程式)
→ C 后端 / JS 后端
Nim 的 JS 后端生成可读的惯用 JS,支持 {.importjs.} pragma 直接嵌入 JS 表达式。由于 Nim 的宏系统在 compile-time 执行,某些 meta-programming 模式可以零运行时开销地展开到 JS。Nim 不提供 Wasm 原生路线(需要通过 Emscripten 的 C 路线)。
GopherJS(Go → JavaScript)
GopherJS 的核心问题是:Go 的运行时假设与 JS 环境根本不兼容。
Go 源代码
→ Go 编译器前端(类型检查)
→ GopherJS IR
→ JavaScript(携带完整 Go 运行时:goroutine 调度器、channel、GC)
GopherJS 必须在 JS 中模拟整个 Go 运行时,包括:协程调度(基于 continuation 变换)、goroutine 栈增长、channel 通信。这导致产出的 JS 体积极大(一个 Hello World 约 44KB),且性能低于等效原生 Go 约 30%。GopherJS 的维护重心已经部分转移到 GOOS=js GOARCH=wasm 官方 Wasm 目标(同样携带完整运行时,但在 Wasm 环境中更自然)。
路线 E:BEAM / 函数式系(ClojureScript · Gleam)
ClojureScript
ClojureScript 是 Clojure(JVM Lisp)的 JS 目标,通过 Google Closure Compiler 的高级优化模式:
ClojureScript 源代码
→ ClojureScript 编译器(JVM 上运行)
→ Google Closure Compiler AST(JS AST 格式)
→ Closure 高级优化(整程序 dead code elimination、property renaming)
→ 优化的 JavaScript
ClojureScript 的特殊之处:它把 Google Closure Compiler 作为优化器而非仅仅是打包工具,利用其高级模式做整程序分析和积极的 dead code elimination。这使 ClojureScript 在生产构建体积上远小于 Kotlin/JS。
不变数据结构(PersistentVector、PersistentHashMap)全部以 JS 对象实现,有运行时开销,但结合 React 的不可变 diff 模型极为自然。
Gleam
Gleam(2024年3月 v1.0 发布)的 JS 编译路线是双目标(Erlang BEAM + JavaScript)的产物:
Gleam 源代码
→ Gleam 编译器(Rust 实现,单一二进制)
→ Gleam AST(类型化,包含平台特定注解)
→ 目标后端:
Erlang 后端 → .erl 文件(在 BEAM 上运行)
JS 后端 → .mjs 文件(ES module)
Gleam 的 JS 输出非常轻量——它不携带任何运行时库,ADT 直接映射为 JS 对象字面量(tagged union 模式)。但 Gleam 的 JS 后端存在一个根本性约束:BEAM 的并发原语(actor、process、OTP)在 JS 环境中完全缺席。gleam/otp 包只有 Erlang target。这意味着使用 Gleam/JS 时,你享有类型系统和函数式语义,但失去了 Gleam 对 BEAM 生态的最大价值主张。
2025年 Stack Overflow 调查显示 Gleam 以 70% 开发者满意度排名第二(仅次于 Rust 72%),主要驱动力是极佳的 DX(错误信息、工具链一体化、学习曲线)。
路线 F:Wasm 优先 / 纯 SSA 路线(MoonBit wasm-gc,续前)
MoonBit 已在路线 A 中覆盖。这里补充其作为"Wasm 原生优先"语言的独特设计原理:
MoonBit 的设计者 Hongbo Zhang 的核心论点是:现有语言(Go、Rust、Java)设计时未考虑 WasmGC 的语义约束,因此编译到 Wasm 时需要携带自己的 GC 或做大量适配,产出的 Wasm 二进制巨大且低效。MoonBit 则从类型系统层面就与 WasmGC 的类型层次(struct、array、managed heap)同构,使得编译到 Wasm 几乎是直接映射。
三、六大路线系统比较矩阵
| 维度 |
ReScript |
Kotlin/JS |
Scala.js |
Fable |
MoonBit |
Haxe |
Nim |
GopherJS |
ClojureScript |
Gleam |
| 路线归属 |
ML/OCaml |
JVM多后端 |
JVM多后端 |
ML/OCaml |
Wasm原生 |
多目标平台 |
原生转译 |
原生转译 |
BEAM函数式 |
BEAM函数式 |
| 中层 IR |
Lambda IR |
Kotlin IR |
SJSIR |
Fable AST |
MoonBit IR |
类型化 AST |
Nim IR |
GopherJS IR |
Closure AST |
Gleam AST |
| IR 形式 |
树+有限SSA |
树+lowering |
树+链接器 |
树 |
SSA-like |
树 |
图IR |
树 |
JS AST格式 |
树 |
| 类型系统 |
Nominal/sound |
structural |
structural |
structural |
Nominal/sound |
structural |
structural |
dynamic |
dynamic |
Nominal/sound |
| JS 可读性 |
★★★★★ |
★★★☆☆ |
★★★☆☆ |
★★★★☆ |
★★★☆☆ |
★★★★★ |
★★★★☆ |
★★☆☆☆ |
★★★☆☆ |
★★★★☆ |
| JS 互操作 |
★★★★★ |
★★★★☆ |
★★★★☆ |
★★★★★ |
★★★☆☆ |
★★★★★ |
★★★★☆ |
★★★★★ |
★★★★☆ |
★★★☆☆ |
| Wasm 路线 |
无 |
一流(wasm-gc) |
实验性 |
间接(→Rust→Wasm) |
一流(wasm-gc) |
无直接支持 |
间接(→C→Wasm) |
无 |
无 |
无 |
| 编译速度 |
★★★★★ |
★★★☆☆ |
★★★☆☆ |
★★★★☆ |
★★★★★ |
★★★★☆ |
★★★★☆ |
★★★☆☆ |
★★★☆☆ |
★★★★★ |
| 产出体积 |
极小 |
大 |
中 |
小 |
极小(Wasm) |
小 |
中 |
极大 |
中 |
极小 |
| 整程序优化 |
有限(文件级) |
有(IR级) |
有(链接期) |
有限 |
有 |
有(DCE) |
有限 |
无 |
有(Closure高级) |
无 |
四、横切关注点:三个根本性张力
从六条路线的比较中,三个根本性张力反复出现:
张力 1:语义保留 vs. 互操作透明度
语义保留度高的语言(ReScript 的 nominal 类型、Gleam 的 ADT、MoonBit 的 struct)在 JS 互操作时必须引入类型边界。用户需要在边界处做显式转换,或者接受 FFI 逃逸到动态类型。语义保留度低的语言(GopherJS、Nim)生成更接近 JS 惯用代码,但源语言的某些保证在 JS 层面消失。
这是不可消解的设计权衡,没有银弹——但统一 IR 可以让这个权衡的位置从各语言的代码生成层提升到 IR 语义层,使其更可见、可配置。
张力 2:文件级快速编译 vs. 整程序优化
ReScript 和 Gleam 的文件级独立编译使增量构建极快,但放弃了跨文件的内联、逃逸分析和常量传播。Scala.js 的链接期优化、ClojureScript 对 Closure 高级模式的依赖,代价是整体构建时间延长。
WasmGC 的出现部分缓解了这个张力:Wasm 模块系统支持真正的增量链接,使得"快速构建 + 充分优化"成为可能,但需要 IR 设计支持跨模块类型流分析。
张力 3:JS 互操作 vs. Wasm 高性能
JS 互操作依赖 JS 对象模型(动态属性、原型链、隐式类型转换)。Wasm(尤其是 WasmGC)的类型系统与 JS 对象模型存在根本性的阻抗失配:Wasm struct 对 JS 是不透明的 externref,无法直接访问字段。Scala.js 在实现 Wasm 后端时面临这个问题的最尖锐形式——@JSExport 在 Wasm 目标上必须放弃。
解决这个张力的唯一正确方向是 Wasm Component Model:通过 WIT(WebAssembly Interface Types)定义高层类型接口,允许 Wasm 模块与 JS 以结构化类型而非原始指针交互。MoonBit 的 use-js-builtin-string 选项是这个方向的早期探索。
五、统一中层 SSA IR 架构设计
5.1 设计目标
一个面向 JS/Wasm 双目标的统一中层 SSA IR 应当满足:
- 表达力充分:能够无损表达 OCaml/Haskell 级别的 ADT、模式匹配、高阶函数、GC 语义
- 可优化:支持 constant folding、DCE、inline、逃逸分析、load/store elimination
- 双后端可实现:从同一 IR 出发,JS 后端和 WasmGC 后端可以独立实现,无需在 IR 层面做目标特定妥协
- 互操作注解友好:JS FFI 调用和 Wasm import/export 都可以在 IR 层面表达为一等公民,而非特殊情况
- 增量友好:支持模块化编译,模块接口可序列化(类似 ReScript 的
.cmj / Scala.js 的 .sjsir 文件)
5.2 IR 核心结构
命名为 JSWASMSSA(或简称 JWS IR):
Module
└─ FunctionDef+
├─ Signature:(param-types, return-type, effect-annotation)
├─ BasicBlock+(SSA 形式)
│ ├─ Phi 节点(φ 函数,在 join point 合并值)
│ └─ Instruction+
│ ├─ 值定义(每个值 exactly-once 赋值)
│ └─ Terminator(branch / return / throw / tailcall)
├─ TypeDef 引用(本地 ADT、struct、enum)
└─ FFI 注解(js-import / wasm-import / wasm-export)
TypeSystem
├─ Primitive:i32 / i64 / f64 / bool / unit / never
├─ GCRef<T>:指向 GC 管理堆对象的引用(对应 WasmGC externref/anyref)
├─ FuncRef<Sig>:可调用引用(对应 WasmGC funcref)
├─ Struct{fields: [(name, type, mutability)]}:命名积类型
├─ Variant{cases: [(tag: i32, payload: type?)]}:和类型(ADT)
├─ Array<T>:同质数组(GC 管理)
├─ JSAny:逃逸到 JS 动态类型的特殊类型
└─ Nullable<T>:明确的可空性( nullable 是显式注解,非默认)
5.3 关键设计决策
决策 1:JSAny 作为隔离边界
JSAny 类型是 IR 的"脏舱":任何从 JS 世界传入的值必须先被标注为 JSAny,在 IR 中的使用必须经过显式的 cast<T> 或 js-property-access 节点。这使得:
- JS 后端:
JSAny 直接穿透为 JS 的动态值,cast 编译为运行时检查(或在类型安全证明后消除)
- Wasm 后端:
JSAny 编译为 externref,需要通过 Wasm-JS 边界函数处理;cast 编译为显式的 JS 调用
这个设计使互操作代价在 IR 层面可见,而非隐藏在代码生成器里。
决策 2:Variant 直接编码 ADT,不通过 tagged 对象
大多数 compile-to-JS 语言将 ADT 编译为 {tag: "Foo", field1: x, field2: y} 形式的 JS 对象。这在 JS 后端是自然的,但在 WasmGC 后端极其低效(需要动态属性访问)。
JWS IR 的 Variant 类型在 IR 层面就是一等的 sum type:
-- IR 表示
%result : Variant{Ok: i32, Err: GCRef<String>}
%result = switch %tag {
0 => %ok_variant = inject-variant<Ok, %value>
1 => %err_variant = inject-variant<Err, %error>
}
-- JS 后端输出
const result = tag === 0 ? {TAG: 0, _0: value} : {TAG: 1, _0: error}
-- WasmGC 后端输出
(struct.new $Variant_Ok (local.get $value))
两个后端从同一 IR 节点出发,选择各自最优的表示。
决策 3:Effect 注解替代异常模型
JS 的异常(throw/catch)和 Wasm 的异常处理(tag + exnref)有不同的语义和性能特征。JWS IR 引入轻量的 Effect 注解:
@effect(throws: {ErrorType: GCRef<Error>}, async: bool, pure: bool)
fn parse(input: GCRef<String>) -> Variant{Ok: GCRef<AST>, Err: GCRef<ParseError>>
- JS 后端:
throws effect 编译为 try/catch,async 编译为 async/await
- Wasm 后端:
throws effect 编译为 Wasm Exception Handling proposal 的 throw/catch 指令
- pure 注解:允许优化器做跨调用点的 common subexpression elimination
决策 4:双态 GCRef(managed / external)
GCRef<T, managed> -- 由本语言运行时 GC 管理的对象(Wasm 后端 → WasmGC struct)
GCRef<T, external> -- 来自 JS 环境的对象(Wasm 后端 → externref,JS 后端 → 直接引用)
这个区分使 Wasm 后端能精确生成 struct.new vs. externref,避免不必要的 boxing/unboxing。
5.4 优化 Pass 设计
在 JWS IR 层面,以下优化 Pass 对 JS 和 Wasm 后端都有价值:
-
常量折叠 + 常量传播(Sparse Conditional Constant Propagation,SCCP)
- 消除
1 + 2 → 3,传播已知常量穿越 branch
-
Dead Code Elimination(通过 SSA def-use 链实现)
- 无 use 的 def 直接删除,比 AST 级 DCE 更精确
-
函数内联(基于 call site 频率注解和 IR 大小估计)
-
逃逸分析(针对 GCRef<T, managed>)
- 未逃逸出函数的对象可以在 JS 后端 stack-allocate(对象字面量),在 Wasm 后端保持 local 变量
-
Variant 特化(当 Variant 在调用点的 tag 静态已知时)
-
Phi 合并(Redundant Phi Elimination)
5.5 后端代码生成策略
JS 后端
从 JWS IR 生成 ES module JS 的关键映射:
| JWS IR 节点 |
JS 输出 |
BasicBlock + Phi |
合并为 let / const 声明序列,分支为 if/else 或条件表达式 |
Struct |
JS 对象字面量 {field: val} 或 class |
Variant |
Tagged 对象 {TAG: n, _0: x} |
FuncRef |
Arrow function 或 closure |
GCRef<T, external> |
直接 JS 引用,无包装 |
JSAny cast |
typeof x === 'T' ? x : throw 或 noop |
Effect throws |
try { } catch(e) { } |
Effect async |
async/await |
| Tail call |
显式 while (true) trampoline 或 JS 原生 TCO(strict mode) |
WasmGC 后端
| JWS IR 节点 |
Wasm 输出 |
BasicBlock |
标准 Wasm block/loop/if 结构 |
Phi |
对应 block 参数(Wasm 2.0 multi-value) |
Struct |
(struct.new $T ...) |
Variant |
对应 (struct.new $Variant_Case ...) + GC 子类型关系 |
FuncRef |
(func.ref $f) 或 (ref.func ...) |
GCRef<T, managed> |
WasmGC (ref $T) |
GCRef<T, external> |
externref |
JSAny |
externref + JS import |
Effect throws |
Wasm Exception Handling 的 throw/try_table |
Effect async |
协程变换(Wasm Stack Switching proposal,待标准化) |
5.6 模块接口序列化
每个编译单元产出两个制品:
.jws:二进制序列化的 JWS IR(携带类型信息,用于 LTO 和跨模块内联)
.jwsi:模块接口(类似 .cmi / .d.ts):导出符号的类型签名,不包含实现
构建系统基于 .jwsi 做精确增量重编译(类似 ReScript 的 .cmj 依赖追踪)。.jws 文件用于全程序优化 Pass(类似 Scala.js 的链接器阶段)。
六、当前技术路线与统一 IR 的对应关系
各路线现有 IR 与 JWS IR 的可能映射:
| 语言 |
现有 IR |
映射到 JWS IR 的工作量 |
| ReScript |
Lambda IR |
中:Lambda IR 已接近 SSA,需要显式 Phi 插入和 JSAny 边界标注 |
| Scala.js |
SJSIR |
低:SJSIR 本身有明确的 JS 互操作节点,可直接映射到 JWS 的 JSAny 和 GCRef |
| Kotlin/JS |
Kotlin IR |
低:Kotlin IR 已是多后端 IR,JWS 可作为其 JS/Wasm 后端的公共中间层 |
| Fable |
Fable AST |
中:Fable AST 是树形结构,需要 ANF/SSA 变换 |
| MoonBit |
MoonBit IR |
极低:MoonBit IR 设计就与 WasmGC 类型系统高度对齐,几乎可直接降级到 JWS |
| Gleam |
Gleam AST |
高:Gleam AST 尚未引入显式的 Wasm 路线,需要完整后端实现 |
七、结语:技术路线选择的决策框架
基于以上分析,建议开发者按以下框架选择技术路线:
如果你的首要关注是 JS 生态互操作透明度:选择 Fable(F#语义 + 一流 JS 互操作)或 Haxe(多目标 + 成熟 externs 系统)
如果你的首要关注是类型系统健全性:选择 ReScript(nominal + sound,极快编译)或 MoonBit(同时支持 Wasm 一流)
如果你的首要关注是 Wasm 性能:选择 MoonBit(专为 WasmGC 设计)或 Kotlin/Wasm(成熟工具链 + Compose Multiplatform)
如果你的首要关注是并发/分布式系统:选择 Gleam(BEAM actor + 类型安全,但 JS 目标功能受限)
如果你已有 JVM 代码库:选择 Scala.js(成熟 + 实验性 Wasm)或 Kotlin(JS + Wasm 双目标)
统一 SSA IR 的实际价值不在于替代这些语言各自的 IR,而在于提供一个语言无关的分析和优化框架,以及一个明确规范化的 JS/Wasm 互操作语义层——这恰恰是当前所有路线都不同程度缺乏的。随着 WasmGC、Wasm Component Model 和 JS Interop 提案的成熟,这个统一层的工程价值将在未来三到五年内显著提升。
报告基于截至 2025 年 4 月的公开技术文档、编译器源码和社区讨论。ReScript 12.0 (2025-12)、Scala.js 1.19 (2025)、Kotlin/Wasm Beta (2025-09)、MoonBit 开源 Wasm 后端 (2024-12)、Gleam v1.x (2024-03+) 的最新架构均已纳入分析。
编译到 JavaScript / Wasm:六大技术路线系统比较与统一中层 SSA IR 架构设计
一、问题框架:为什么需要系统性比较
"编译到 JavaScript" 这个标签掩盖了底层极其异质的技术决策空间。当一个语言决定将 JS 作为编译目标时,它实际上在五个维度上做出了相互耦合的设计选择:
这五个维度的选择,决定了生成代码的可读性、体积、性能以及 debug 体验。本报告将十种语言归纳为六条技术路线,逐一解剖其架构,最后提出一个面向 JS/Wasm 双目标的统一中层 SSA IR 设计方案。
二、六大技术路线
路线 A:ML系 / OCaml血统(ReScript · Fable · MoonBit)
这三个语言有一个共同的血统节点:它们的创始人都深度参与过 OCaml 编译器生态。
ReScript
ReScript 12(2025年底发布)的编译管道如下:
关键设计选择:
.res文件独立编译,产出.cmi(接口)、.cmt(类型树)、.cmj(优化元数据)、.mjs(JS 输出)四个独立制品。没有全程序分析阶段,无需链接器。option<t>在 JS 层面直接是null | t,无包装。@rescript/runtime是一个极轻量的辅助包,核心逻辑全部在编译期内联。ReScript 不通过中间 SSA,而是在 Lambda IR 层做有限优化后直接生成 JS。这是速度的来源,也是整程序优化的天花板。
Fable(F# → JS / Python / Rust / Dart)
Fable 的架构依托 FSharp Compiler Services(FCS):
Fable 的核心创新是 Fable AST:这是一个比 FCS 的 TAST 更低层但比目标语言更高层的表示,在这一层做 F# 特有的语义转换——currying 展开、Union Type 编译策略(tagged object vs. plain value)、
Option<T>擦除。Fable 特别强调 可读性:生成的 JS 是符合 ES2015 规范的惯用代码,开发者可以 debug,可以用任何 bundler 处理。代价是某些 F# 语义(如 structural equality)需要运行时辅助函数,引入少量运行时开销。
MoonBit
MoonBit(2024年底开源 Wasm 后端)是这三者中唯一一个以 Wasm 为第一公民设计的语言:
MoonBit 的设计哲学是:WasmGC 的类型系统(struct、array、GC-managed heap objects)与 MoonBit 的值类型系统高度同构,因此可以做到几乎零开销的语义映射。生成的 Wasm 二进制极小——这是 Hongbo Zhang(前 OCaml 核心贡献者、ReScript 创始人)的核心设计目标。
路线 B:JVM系 / 多后端 IR(Kotlin/JS · Scala.js)
这两个语言都来自 JVM 生态,但采用了截然不同的 IR 策略。
Kotlin/JS + Kotlin/Wasm
Kotlin 编译器有 K1/K2 两个前端和四个后端(JVM、JS、Native、Wasm)。核心是共享的 Kotlin IR:
Kotlin IR 是真正的"一个 IR,多个后端"设计。JS 后端与 Wasm 后端共享相同的 lowering 基础设施(lazy 属性初始化、inline class 展开、协程状态机等),然后各自分叉做目标特定的代码生成。
Kotlin/Wasm 的关键突破是 2024 年 12 月 Safari 完成对 WasmGC 的支持,使 Kotlin/Wasm 应用能在所有主流浏览器运行。JetBrains 基准测试显示 Kotlin/Wasm 在 UI 密集型场景比 Kotlin/JS 快约 3 倍。
Kotlin/JS 的历史包袱是体积问题:stdlib 全量打包进去,一个 "Hello World" 的 JS 产出可能比等效 TypeScript 大 10 倍以上。这是携带整个 Kotlin 标准库 JS 实现的代价。
Scala.js + Scala.js Wasm Backend
Scala.js 的架构基于一个精心设计的专有 IR——SJSIR(Scala.js IR):
SJSIR 的设计极为务实:它是 Scala 对 JS 语义的精确建模,而非通用的低层 IR。SJSIR 明确包含:
nullable: Booleanflag)Wasm 后端的挑战在于:SJSIR 包含大量 JS 互操作语义(
@JSExport、字符串拼接中的隐式 toString 等),这些在 Wasm 沙箱中必须通过 JS 胶水层实现。Scala.js 1.19(2025年)已将 Wasm 后端的性能提升到在计算密集型场景超越 JS 后端。路线 C:多目标平台系(Haxe)
Haxe(2005年,OCaml 实现)是 compile-to-multiple-targets 的先驱:
Haxe 的核心是 externs 系统:用 Haxe 类型注解描述外部平台 API,实现跨目标的类型安全 FFI。生成的 JS 代码极为干净(基准测试中常接近手写 JS 速度),因为 Haxe 在 AST 层面做了大量 inlining 和常量折叠。
Haxe 没有明确的 SSA IR:优化发生在类型化 AST 层面。这使其在单一代码库多目标部署场景(游戏引擎、跨端框架)中有独特优势,但也意味着深度数据流优化较难实现。
路线 D:原生语言→JS 转译(Nim · GopherJS)
Nim
Nim 的 JS 目标是其多后端(C / C++ / ObjC / JS)之一:
Nim 的 JS 后端生成可读的惯用 JS,支持
{.importjs.}pragma 直接嵌入 JS 表达式。由于 Nim 的宏系统在 compile-time 执行,某些 meta-programming 模式可以零运行时开销地展开到 JS。Nim 不提供 Wasm 原生路线(需要通过 Emscripten 的 C 路线)。GopherJS(Go → JavaScript)
GopherJS 的核心问题是:Go 的运行时假设与 JS 环境根本不兼容。
GopherJS 必须在 JS 中模拟整个 Go 运行时,包括:协程调度(基于 continuation 变换)、goroutine 栈增长、channel 通信。这导致产出的 JS 体积极大(一个 Hello World 约 44KB),且性能低于等效原生 Go 约 30%。GopherJS 的维护重心已经部分转移到
GOOS=js GOARCH=wasm官方 Wasm 目标(同样携带完整运行时,但在 Wasm 环境中更自然)。路线 E:BEAM / 函数式系(ClojureScript · Gleam)
ClojureScript
ClojureScript 是 Clojure(JVM Lisp)的 JS 目标,通过 Google Closure Compiler 的高级优化模式:
ClojureScript 的特殊之处:它把 Google Closure Compiler 作为优化器而非仅仅是打包工具,利用其高级模式做整程序分析和积极的 dead code elimination。这使 ClojureScript 在生产构建体积上远小于 Kotlin/JS。
不变数据结构(PersistentVector、PersistentHashMap)全部以 JS 对象实现,有运行时开销,但结合 React 的不可变 diff 模型极为自然。
Gleam
Gleam(2024年3月 v1.0 发布)的 JS 编译路线是双目标(Erlang BEAM + JavaScript)的产物:
Gleam 的 JS 输出非常轻量——它不携带任何运行时库,ADT 直接映射为 JS 对象字面量(tagged union 模式)。但 Gleam 的 JS 后端存在一个根本性约束:BEAM 的并发原语(actor、process、OTP)在 JS 环境中完全缺席。
gleam/otp包只有 Erlang target。这意味着使用 Gleam/JS 时,你享有类型系统和函数式语义,但失去了 Gleam 对 BEAM 生态的最大价值主张。2025年 Stack Overflow 调查显示 Gleam 以 70% 开发者满意度排名第二(仅次于 Rust 72%),主要驱动力是极佳的 DX(错误信息、工具链一体化、学习曲线)。
路线 F:Wasm 优先 / 纯 SSA 路线(MoonBit wasm-gc,续前)
MoonBit 已在路线 A 中覆盖。这里补充其作为"Wasm 原生优先"语言的独特设计原理:
MoonBit 的设计者 Hongbo Zhang 的核心论点是:现有语言(Go、Rust、Java)设计时未考虑 WasmGC 的语义约束,因此编译到 Wasm 时需要携带自己的 GC 或做大量适配,产出的 Wasm 二进制巨大且低效。MoonBit 则从类型系统层面就与 WasmGC 的类型层次(struct、array、managed heap)同构,使得编译到 Wasm 几乎是直接映射。
三、六大路线系统比较矩阵
四、横切关注点:三个根本性张力
从六条路线的比较中,三个根本性张力反复出现:
张力 1:语义保留 vs. 互操作透明度
语义保留度高的语言(ReScript 的 nominal 类型、Gleam 的 ADT、MoonBit 的 struct)在 JS 互操作时必须引入类型边界。用户需要在边界处做显式转换,或者接受 FFI 逃逸到动态类型。语义保留度低的语言(GopherJS、Nim)生成更接近 JS 惯用代码,但源语言的某些保证在 JS 层面消失。
这是不可消解的设计权衡,没有银弹——但统一 IR 可以让这个权衡的位置从各语言的代码生成层提升到 IR 语义层,使其更可见、可配置。
张力 2:文件级快速编译 vs. 整程序优化
ReScript 和 Gleam 的文件级独立编译使增量构建极快,但放弃了跨文件的内联、逃逸分析和常量传播。Scala.js 的链接期优化、ClojureScript 对 Closure 高级模式的依赖,代价是整体构建时间延长。
WasmGC 的出现部分缓解了这个张力:Wasm 模块系统支持真正的增量链接,使得"快速构建 + 充分优化"成为可能,但需要 IR 设计支持跨模块类型流分析。
张力 3:JS 互操作 vs. Wasm 高性能
JS 互操作依赖 JS 对象模型(动态属性、原型链、隐式类型转换)。Wasm(尤其是 WasmGC)的类型系统与 JS 对象模型存在根本性的阻抗失配:Wasm struct 对 JS 是不透明的 externref,无法直接访问字段。Scala.js 在实现 Wasm 后端时面临这个问题的最尖锐形式——
@JSExport在 Wasm 目标上必须放弃。解决这个张力的唯一正确方向是 Wasm Component Model:通过 WIT(WebAssembly Interface Types)定义高层类型接口,允许 Wasm 模块与 JS 以结构化类型而非原始指针交互。MoonBit 的
use-js-builtin-string选项是这个方向的早期探索。五、统一中层 SSA IR 架构设计
5.1 设计目标
一个面向 JS/Wasm 双目标的统一中层 SSA IR 应当满足:
.cmj/ Scala.js 的.sjsir文件)5.2 IR 核心结构
命名为 JSWASMSSA(或简称 JWS IR):
5.3 关键设计决策
决策 1:JSAny 作为隔离边界
JSAny类型是 IR 的"脏舱":任何从 JS 世界传入的值必须先被标注为JSAny,在 IR 中的使用必须经过显式的cast<T>或js-property-access节点。这使得:JSAny直接穿透为 JS 的动态值,cast 编译为运行时检查(或在类型安全证明后消除)JSAny编译为externref,需要通过 Wasm-JS 边界函数处理;cast 编译为显式的 JS 调用这个设计使互操作代价在 IR 层面可见,而非隐藏在代码生成器里。
决策 2:Variant 直接编码 ADT,不通过 tagged 对象
大多数 compile-to-JS 语言将 ADT 编译为
{tag: "Foo", field1: x, field2: y}形式的 JS 对象。这在 JS 后端是自然的,但在 WasmGC 后端极其低效(需要动态属性访问)。JWS IR 的
Variant类型在 IR 层面就是一等的 sum type:两个后端从同一 IR 节点出发,选择各自最优的表示。
决策 3:Effect 注解替代异常模型
JS 的异常(throw/catch)和 Wasm 的异常处理(tag + exnref)有不同的语义和性能特征。JWS IR 引入轻量的 Effect 注解:
throwseffect 编译为try/catch,async编译为async/awaitthrowseffect 编译为 Wasm Exception Handling proposal 的throw/catch指令决策 4:双态 GCRef(managed / external)
这个区分使 Wasm 后端能精确生成
struct.newvs.externref,避免不必要的 boxing/unboxing。5.4 优化 Pass 设计
在 JWS IR 层面,以下优化 Pass 对 JS 和 Wasm 后端都有价值:
常量折叠 + 常量传播(Sparse Conditional Constant Propagation,SCCP)
1 + 2→3,传播已知常量穿越 branchDead Code Elimination(通过 SSA def-use 链实现)
函数内联(基于 call site 频率注解和 IR 大小估计)
逃逸分析(针对 GCRef<T, managed>)
Variant 特化(当 Variant 在调用点的 tag 静态已知时)
Phi 合并(Redundant Phi Elimination)
%x = φ(%a, %a)→%x = %a5.5 后端代码生成策略
JS 后端
从 JWS IR 生成 ES module JS 的关键映射:
BasicBlock+ Philet/const声明序列,分支为if/else或条件表达式Struct{field: val}或 classVariant{TAG: n, _0: x}FuncRefGCRef<T, external>JSAnycasttypeof x === 'T' ? x : throw或 noopthrowstry { } catch(e) { }asyncasync/awaitwhile (true)trampoline 或 JS 原生 TCO(strict mode)WasmGC 后端
BasicBlockPhiStruct(struct.new $T ...)Variant(struct.new $Variant_Case ...)+ GC 子类型关系FuncRef(func.ref $f)或(ref.func ...)GCRef<T, managed>(ref $T)GCRef<T, external>externrefJSAnyexternref+ JS importthrowsthrow/try_tableasync5.6 模块接口序列化
每个编译单元产出两个制品:
.jws:二进制序列化的 JWS IR(携带类型信息,用于 LTO 和跨模块内联).jwsi:模块接口(类似.cmi/.d.ts):导出符号的类型签名,不包含实现构建系统基于
.jwsi做精确增量重编译(类似 ReScript 的.cmj依赖追踪)。.jws文件用于全程序优化 Pass(类似 Scala.js 的链接器阶段)。六、当前技术路线与统一 IR 的对应关系
各路线现有 IR 与 JWS IR 的可能映射:
七、结语:技术路线选择的决策框架
基于以上分析,建议开发者按以下框架选择技术路线:
如果你的首要关注是 JS 生态互操作透明度:选择 Fable(F#语义 + 一流 JS 互操作)或 Haxe(多目标 + 成熟 externs 系统)
如果你的首要关注是类型系统健全性:选择 ReScript(nominal + sound,极快编译)或 MoonBit(同时支持 Wasm 一流)
如果你的首要关注是 Wasm 性能:选择 MoonBit(专为 WasmGC 设计)或 Kotlin/Wasm(成熟工具链 + Compose Multiplatform)
如果你的首要关注是并发/分布式系统:选择 Gleam(BEAM actor + 类型安全,但 JS 目标功能受限)
如果你已有 JVM 代码库:选择 Scala.js(成熟 + 实验性 Wasm)或 Kotlin(JS + Wasm 双目标)
统一 SSA IR 的实际价值不在于替代这些语言各自的 IR,而在于提供一个语言无关的分析和优化框架,以及一个明确规范化的 JS/Wasm 互操作语义层——这恰恰是当前所有路线都不同程度缺乏的。随着 WasmGC、Wasm Component Model 和 JS Interop 提案的成熟,这个统一层的工程价值将在未来三到五年内显著提升。
报告基于截至 2025 年 4 月的公开技术文档、编译器源码和社区讨论。ReScript 12.0 (2025-12)、Scala.js 1.19 (2025)、Kotlin/Wasm Beta (2025-09)、MoonBit 开源 Wasm 后端 (2024-12)、Gleam v1.x (2024-03+) 的最新架构均已纳入分析。