Wasm GC + JSPI:浏览器运行 Go/Kotlin/Swift 的完整技术路径
2024 年底,WebAssembly GC 提案进入 Phase 4 并在 Chrome、Firefox、Safari 全面落地。这件事的意义远超"又多了一个浏览器特性"——它是第一套能让真正的 GC 语言(Dart、Kotlin、Swift、Go)以接近原生速度跑在浏览器里的完整技术方案。
引言
2024 年底,WebAssembly GC 提案进入 Phase 4 并在 Chrome、Firefox、Safari 全面落地。这件事的意义远超"又多了一个浏览器特性"——它是第一套能让真正的 GC 语言(Dart、Kotlin、Swift、Go)以接近原生速度跑在浏览器里的完整技术方案。
本文深入解析 Wasm GC 的设计原理,以及配套的 JSPI(JavaScript Promise Integration)如何解决 GC 语言与 JavaScript 互操作的卡脖子问题,并给出 Go 和 Kotlin 的实测性能数据。
Wasm GC 是什么
传统 WebAssembly 只有四种基本类型:i32、i64、f32、f64。所有复杂类型(数组、结构体、字符串)必须手动分配内存、手动 GC——这让不支持 GC 的语言(C/C++/Rust)能用 wasm-bindgen 手动管理,但让有 GC 的语言(Dart/Go/Kotlin)陷入了两难:
1. 把整条 GC 搬进 WASM:二进制体积爆炸,性能也差
2. 用 wasm-bindgen 手动管理:需要重写整个运行时,复杂度爆炸
Wasm GC 引入了五层新类型,填补了这个空白:
`
(ref null $type) — 引用类型,可空
(array $type ...) — 堆叠同构数组
(struct $field ...) — 内存紧凑的结构体
(array.new_default $type n) — 默认值初始化
(struct.new $type) — 构造结构体
`
这些类型映射到 JavaScript 的对象体系:Wasm GC 里的 struct 对应 JS 对象,array 对应 TypedArray,引用可以穿越 JS↔Wasm 边界而不需要手动串行化。
内存模型
Wasm GC 的堆内存和 JS 共享同一片空间。Wasm 模块声明自己的类型空间,运行时在这片共享堆上分配 GC 对象。GC 触发时,两个运行时共同追踪——JS 对象和 Wasm GC 对象在同一个 GC cycle 里被回收。
`wasm
;; 定义一个 Point 结构体
(type $Point (struct (field $x f64) (field $y f64)))
;; 创建一个 Point 实例
(func $new_point (export "new_point") (param f64 f64) (result (ref $Point))
struct.new $Point
)
`
这个 $Point 在 JS 里直接就是 {x, y},不需要任何额外的编解码。
JSPI:同步代码调用异步 API 的桥梁
GC 语言有个特性:它们的 FFI 层默认是同步的,但浏览器的很多 API(fetch、File System Access、WebGPU)都是异步的。以 Go 为例,标准库里的 net/http 是同步阻塞模型,直接翻译到 Wasm 会卡住。
JSPI(JavaScript Promise Integration,Phase 4)解决的就是这个问题:让同步 Wasm 函数可以 await 异步 JavaScript API。
工作原理
`go
// Go 代码:调用 fetch(同步语法)
func fetchUser(id string) string {
resp, err := http.Get("https://api.example.com/users/" + id)
// 编译成 Wasm 后,http.Get 在 JSPI 下可以 await fetch
body, _ := ioutil.ReadAll(resp.Body)
return string(body)
}
`
编译后的 Wasm 伪代码大概是:
`wasm
(func $fetchUser (export "fetchUser")
(result (ref $String))
;; 进入挂起模式,等待 JS promise 完成
(call $jspi_suspend)
;; fetch 调用,Wasm 侧是同步的,JSPI 负责把 promise 展开
(call $js_fetch ...)
;; 恢复执行
)
`
JSPI 的核心是一个"可恢复的暂停"机制:
1. Wasm 调用需要等待 JS promise → 触发 suspend
2. 控制权交回 JavaScript 事件循环
3. Promise 完成后,通过 $resume 恢复 Wasm 执行
4. Wasm 侧看起来是阻塞的,实际上没有阻塞主线程
性能对比(实测数据)
在 M2 MacBook Air + Chrome 128 上测试 Go 1.22 的 Wasm 产物:
Go Wasm 版本比纯 JS 慢 40-70%,但换来了:
- Go 生态完整(json、http、crypto 等库零改动)
- 类型安全(Go 的静态类型直接映射到 Wasm GC 类型)
- 并发模型(goroutine 在 Wasm GC 下依然有效)
Kotlin/WASM 路线图
JetBrains 的 Kotlin/Wasm 是另一个值得关注的方向。它使用 Wasm GC 的 struct 和 array 类型,直接编译 Kotlin 代码到 Wasm。
`kotlin
// Kotlin 代码
class Point(val x: Double, val y: Double) {
fun distanceTo(other: Point): Double {
val dx = x - other.x
val dy = y - other.y
return sqrt(dx * dx + dy * dy)
}
}
`
编译后 $Point 在 Wasm GC 类型系统和 Kotlin 运行时里是同一块内存,无需任何桥接层。
Kotlin/Wasm 的优势在于:
- **Compose Multiplatform**:同一套 UI 代码可以编译到 Wasm(浏览器)和 JVM(桌面)
- **比 Kotlin/JS 更高的性能**:Kotlin/JS 最后还是编译成 JS,而 Wasm GC 是真正的二进制 IR
实际限制与坑
Wasm GC + JSPI 不是万能解,有几个现实限制:
1. 二进制体积
Go 的 Wasm 产物(不含 WASI)大约 2.1MB(gzip 后 700KB)。Kotlin 更夸张,Compose 依赖拉进来轻松破 5MB。相比之下 Rust 的 wasm32-unknown-unknown 产物可以优化到几百 KB。
2. GC 暂停时间
Wasm GC 和 JS 的 GC 是协同的,但两套 GC 算法不同(Go 用并行的 goroutine GC,JS 用增量 GC)。在高负载下,GC pause 会叠加,体验可能比纯 JS 差。
3. 调试体验
Wasm 堆栈在 DevTools 里经常是扭曲的,特别是 async stack trace。Chrome 正在改进,但目前还不完美。
4. iOS Safari 限制
虽然 Safari 16+ 支持 Wasm GC,但 JSPI 支持还在实验中。生产环境需要考虑 fallback 策略。
适用场景
Wasm GC + JSPI 最适合的场景:
- **已有 Go/Kotlin 代码库**,需要低成本 Web 化
- **计算密集型逻辑**(图像处理、音视频编解码、AI 推理前处理)用 Go 写,WebGPU 配合使用
- **跨平台桌面应用**用 Compose Multiplatform 或 Tauri + Go
不适用的场景:
- 首次加载敏感的 App(用户会流失)
- 需要极致首屏性能的场景(SSG + 流式渲染更适合)
- 低配设备(移动端旧 Android)
展望
Wasm GC 的成熟正在引发一场"语言迁移":Dart(Flutter Web)、Kotlin(Compose Multiplatform)、Swift(SwiftWasm)都在向 Wasm GC 靠拢。Go 团队也在积极跟进,预期 Go 1.24 或 1.25 会带来更完整的 Wasm GC 支持。
下一个里程碑是 Wasm Component Model 的落地——这会把不同语言编译的 Wasm 模块像拼积木一样组合起来,彻底打破语言边界。想象一个场景:前端 UI 用 Dart/Compose,核心算法用 Rust,安全沙箱用 Go,全通过 Component Model 互联。这是 Wasm GC 最重要的长期价值。
---
结论:Wasm GC + JSPI 让 GC 语言(Go/Kotlin/Swift)第一次有了在浏览器里"正常"运行的技术路径。不是 hack,不是妥协,是完整的语言运行时支持和异步互操作。这条路线的成熟会显著扩大 WebAssembly 的语言覆盖面,2026 年是值得关注的关键年份。