Kotlin 的 Wasm 目标平台,浏览器里跑 Kotlin

Kotlin 的 Wasm 目标平台,浏览器里跑 Kotlin

Kotlin 的 Wasm 目标平台,浏览器里跑 Kotlin


Kotlin 的 Wasm 目标平台,浏览器里跑 Kotlin


从 Kotlin/JS 的"二等公民"说起


JetBrains 在 2021 年把 Kotlin 的 WebAssembly 支持搬上台面的时候,很多人第一反应是:终于不用写 Kotlin/JS 了。这个反应本身就挺说明问题的。Kotlin/JS 从 2017 年随着 Kotlin 1.1 发布,到现在也快八年了,但在前端社区的存在感始终有点尴尬。不是不能用,是用了之后总觉得自己在跟整个生态较劲。


Kotlin/JS 的核心痛点在于它跟 JavaScript 的互操作模式。Kotlin 编译成 JS 之后,要跟 npm 生态打交道,要处理 TypeScript 类型定义,要在 webpack 或 vite 的配置里绕来绕去。JetBrains 搞了个 Dukat 工具来自动生成 TypeScript 声明文件的 Kotlin 绑定,但这个项目 2021 年就停止维护了,最后更新停在 Kotlin 1.5 时代。后来推出的 Kotlin/JS IR 编译器后端确实改善了一些情况,但根本问题没解决:你还是在 JavaScript 的 runtime 上跑,GC 行为、异步模型、模块加载,全都受制于 JS 引擎的实现。


我看过一个 GitHub issue,是 Kotlin/JS 用户报的性能问题,说同样一段逻辑在 Node.js 里跑比纯 JS 手写慢 30% 到 50%。JetBrains 的工程师回复说这是预期行为,因为 Kotlin 的 lambda 捕获和对象分配模式在 JS 引擎的优化路径上并不友好。这个回复很诚实,但也很打击人。你选 Kotlin 本来是为了类型安全和工程化能力,结果在浏览器里反而要为这些抽象层买单。


所以 Wasm 的消息出来,很多人的期待是:能不能绕过 JS 这个中间层,直接在浏览器里跑一个更原生的 Kotlin runtime?


Kotlin/Wasm 的技术底牌


JetBrains 在 Kotlin 1.9.20 里把 Kotlin/Wasm 标记为 Alpha,到 2.0.0 还是 Alpha。这个节奏比 Kotlin/Native 当年保守多了,但技术栈确实更复杂。Wasm 本身在 2023 年才随着 WebAssembly Garbage Collection(WasmGC)提案进入主流浏览器的稳定版本,Chrome 119、Firefox 120、Safari 17.1 陆续支持。没有 WasmGC 之前,想在浏览器里跑带 GC 的语言,要么自己带一个 GC 实现(像 AssemblyScript 或者 Rust 的 wasm-bindgen 那样手动管理),要么性能惨不忍睹。Kotlin 显然不可能接受手动管理内存,所以等 WasmGC 落地是必经之路。


Kotlin/Wasm 的编译链路跟 Kotlin/Native 有共享的部分,都用了一个叫 Kotlin/Native 编译器基础设施的东西,但后端代码生成是专门针对 Wasm 的。具体实现上,它依赖了 WasmGC 的 struct 和 array 类型来表达 Kotlin 的对象布局,用 Wasm 的 exception handling 提案来实现 Kotlin 的异常机制。这两个提案在 2023-2024 年间的浏览器支持度并不一致,所以 JetBrains 的兼容性矩阵里有一堆脚注,比如 Safari 的异常处理支持到某个版本才完整。


这里有个细节很多人没注意到:Kotlin/Wasm 目前不支持 Kotlin/Native 的所有目标平台特性。比如你在 Kotlin/Native 里能用的某些 POSIX API、某些线程模型,在 Wasm 里直接没有,因为 Wasm 的线程支持(SharedArrayBuffer + Atomics)和 WASI(WebAssembly System Interface)的标准化进度本身就在拉扯中。JetBrains 的文档里有个表格,明确列出了哪些 kotlinx 库函数在 Wasm 目标上可用,哪些会抛 UnsupportedOperationException。这个诚实度值得肯定,但也意味着你现在用 Kotlin/Wasm,得时刻查兼容性列表,跟当年 Kotlin/Native 早期的情况如出一辙。


Compose Multiplatform 对 Kotlin/Wasm 的支持是另一个观察窗口。JetBrains 在 2024 年初宣布 Compose for Web 基于 Kotlin/Wasm 重新实现,替代了之前基于 Kotlin/JS + HTML DOM 的方案。这个决策本身就很能说明问题:之前的 Compose for Web 性能撑不住,DOM 操作的 overhead 太大,而且 Compose 的重组(recomposition)机制需要频繁创建和丢弃对象,在 JS 的 GC 压力下表现很差。换成 Wasm 之后,Compose 的 runtime 可以跑在自己的 GC 上,Skia 的渲染也通过 WebGL 或即将落地的 WebGPU 走更底层的路径。


但我实际试了一下 Compose for Web 的 Wasm 版本,体验还是磕磕绊绊。Hello World 级别的 demo 能跑,稍微复杂一点的布局,比如带 LazyColumn 的列表,在 Chrome 里的内存占用会稳步攀升,最后 tab 崩溃。不确定是 Skia 的 Wasm 编译版本有内存泄漏,还是 Kotlin/Wasm 的 GC 跟浏览器的交互有问题。GitHub 上有个 open issue #4321(数字我记不清了,类似编号),讨论的就是 Wasm 目标下的内存行为,JetBrains 的回复是"正在调查"。


跟 Kotlin/Native 的微妙关系


Kotlin/Wasm 的出现,让 Kotlin 的多平台版图更拥挤了。现在你有:Kotlin/JVM(Android、服务器)、Kotlin/JS(浏览器、Node.js)、Kotlin/Native(iOS、嵌入式、桌面)、Kotlin/Wasm(浏览器、可能的服务器端 Wasm runtime)。四个后端,四个不同的 runtime 行为,四个不同的调试体验。


JetBrains 的公关话术是"共享的 Kotlin 标准库和公共源代码",但实际开发中,平台差异的裂缝无处不在。比如 kotlinx.coroutines 在 Native 和 Wasm 上的 Dispatcher 实现就不一样,Default 调度器在 Native 上可能受限于线程池大小,在 Wasm 上因为单线程限制干脆就是单线程事件循环。你写 Dispatchers.Default 的时候,心里得清楚这个"默认"在不同平台上完全是两回事。


更微妙的是 Kotlin/Native 的 LLVM 后端和 Kotlin/Wasm 的 Binaryen 后端之间的资源竞争。JetBrains 的编译器团队人力有限,Kotlin/Native 的编译速度、调试符号生成、链接时间优化这些老问题还没完全解决,现在又要分精力去维护 Wasm 后端。我注意到 Kotlin 2.0 发布的时候,Native 的增量编译改进被放在比较靠后的位置,而 Wasm 的新特性(比如对 WASI 的实验性支持)反而有专门的博客文章。这个优先级排序,很难说没有商业考量在里面。


Compose Multiplatform 的桌面目标用的是 Kotlin/JVM + Skiko(Skia 的 JVM 绑定),iOS 目标用的是 Kotlin/Native + Skia 的 Native 编译版本,Web 目标现在切到 Kotlin/Wasm + Skia 的 Wasm 编译版本。同一个 UI 框架,三个完全不同的底层渲染栈,bug 的表现形式也各不相同。我在 macOS 桌面上遇到过 Compose 的文本渲染模糊问题,在 iOS 上遇到过手势识别跟系统冲突的问题,在 Wasm 上遇到过上面说的内存问题。这些不是 Compose 框架本身的逻辑 bug,是平台适配层的摩擦成本。JetBrains 的多平台叙事很美好,但维护这些适配层的工程师显然在负重前行。


浏览器里的 Kotlin,到底图什么


这个问题要拆成两层来看。一层是技术层面的合理性,一层是 JetBrains 作为公司的战略动机。


技术层面,Wasm 给 Kotlin 带来的最大好处是性能可预测性。JS 引擎的 JIT 编译有预热问题,有 deoptimization 陷阱,有 hidden class 转换的 overhead。Wasm 是 AOT 编译的,字节码就是机器码的近似表示,执行路径更稳定。对于计算密集型的场景,比如图像处理、音频分析、游戏逻辑,Wasm 确实有优势。但问题是,这些场景在典型的 Kotlin 应用里占比多高?


Android 开发转过来的工程师,习惯的是 UI 驱动、IO 密集、生命周期复杂的应用模型。这种模型搬到浏览器里,瓶颈通常不在 CPU 计算,而在 DOM 操作、网络延迟、渲染流水线。Wasm 解决的是前者,但前端性能问题的根源往往是后者。你用 Kotlin/Wasm 重写了一个 RecyclerView 级别的列表,发现滚动卡顿,最后查出来是 Skia 的 Wasm 版本在合成层(compositor layer)处理上有问题,或者 WebGL 的上下文切换开销太大。这时候 Wasm 的 AOT 优势完全帮不上忙。


JetBrains 的 demos 里喜欢展示的是 Compose for Web 的动画效果,比如一个带动画的按钮,或者一个可拖拽的卡片。这些 demo 在 M3 MacBook Pro 上确实流畅,但换到 2018 年的 Intel Mac 或者中低端 Android 平板的 Chrome 上,帧率会明显掉。这不是 Kotlin/Wasm 独有的问题,是所有 WebGL 重度应用的共性问题,但它削弱了"Wasm 性能更好"这个卖点的说服力。


战略动机层面,JetBrains 需要 Kotlin 的叙事边界突破 Android。Google 对 Kotlin 的优先支持是 Android 生态的事实标准,但这也成了 Kotlin 的笼子。服务器端有 Go、Rust、Java 本身守着,前端有 TypeScript 的绝对统治,Kotlin 想挤进去,必须找一个差异化切口。Wasm 就是这个切口:它比 JS 快,比 Rust 好写,比 Java 更现代,而且 JetBrains 有 IDE 生态的绑定优势。


但这个差异化有多坚固?TypeScript 的编译速度、工具链成熟度、社区库数量,是 Kotlin/Wasm 短期内不可能追上的。Rust 的 wasm-bindgen 和 leptos、yew 这些框架,已经在探索更高级的 Wasm 前端开发模式,而且 Rust 的性能底牌比 Kotlin 更硬。甚至 Dart 的 Wasm 支持也在推进,Flutter 的 Web 目标迟早会切过去。JetBrains 的时间窗口并不宽裕。


那些没写在文档里的坑


实际用 Kotlin/Wasm 开发,会遇到一些文档不会告诉你的问题。


调试体验是第一道坎。Kotlin/Wasm 编译出来的 .wasm 文件, source map 的支持在 2024 年初还是实验性的。你在 Chrome DevTools 里看到的调用栈,是 Wasm 的函数索引和内存地址,跟 Kotlin 的源代码行号对不上。JetBrains 的解决方案是在 IntelliJ IDEA 里用他们自己的调试协议,通过 Chrome 的 CDP(Chrome DevTools Protocol)来桥接。但这个方案要求你用 IDEA 的特定版本,装特定的插件,而且断点命中率并不稳定。我遇到过断点设在某行,实际停在后面三行的情况,原因是编译器的优化把代码重排了,但 source map 没精确反映。


跟 JavaScript 的互操作是另一个深坑。Kotlin/Wasm 提供了 js() 内联函数和 @JsExport 注解来跟 JS 世界交互,但类型映射的规则很繁琐。Kotlin 的 Long 类型在 JS 里没有精确对应,因为 JS 的 number 是 IEEE 754 double,整数精度只能到 2^53。Kotlin/Wasm 的解决方案是用一个 JS 对象来模拟 64 位整数,但这意味着每次跨边界传递 Long 都要装箱,性能开销显著。如果你在设计一个需要频繁跟 JS 库交互的 Kotlin/Wasm 应用,这个细节会折磨你。


还有文件系统访问。浏览器里的 Wasm 是沙箱环境,没有原生文件系统。Kotlin 的标准库里有 java.io.File 的 expect/actual 映射,但在 Wasm 目标上,这些 API 要么抛异常,要么映射到浏览器的 IndexedDB 或 MemoryFS 的虚拟层。你想读一个用户上传的文件?得先通过 JS 的 File API 拿到 ArrayBuffer,再拷贝到 Wasm 的线性内存里。这个流程的 API 设计在 Kotlin/Wasm 的标准库里还在迭代,不同版本的函数签名会变。


我在一个 side project 里尝试用 Kotlin/Wasm 做一个小型的 Markdown 编辑器,逻辑层用 Kotlin 写,渲染层用 Compose for Web。遇到的一个具体问题是:我想用 kotlinx.serialization 来解析配置 JSON,但 kotlinx.serialization 的 Wasm 支持在 1.6.x 版本里有个 bug,多态序列化(polymorphic serialization)会生成错误的 IR 代码,导致编译器崩溃。这个 bug 在 1.7.0 修掉了,但我花了一下午在 GitHub 上翻 issue 和 commit history 才确认。这种"某个 kotlinx 库的某个版本在 Wasm 上不可用"的情况,目前还是常态。


社区生态的冷启动困境


一门语言的目标平台能不能活,关键看社区有没有动力为它造轮子。Kotlin/Wasm 现在面临的是典型的冷启动问题:用户少,所以库作者没动力支持;库支持少,所以用户不愿意迁移。


以网络库为例。Ktor 是 JetBrains 亲生的 HTTP 客户端/服务器框架,它的客户端部分在 Kotlin/Wasm 上的支持是实验性的。但 Ktor 的 Wasm 实现底层用的是 Fetch API 的 JS 绑定,而不是 Wasm 特有的网络能力(其实目前也没有)。这意味着你在 Kotlin/Wasm 里发 HTTP 请求,走的还是浏览器提供的 JS API,Kotlin 层只是包了一层类型安全的壳。这个价值有,但不大,尤其是跟 TypeScript 里成熟的 axios、ky、ofetch 相比,生态丰富度差太远。


数据库访问更惨淡。SQLDelight 是 Square 出的 Kotlin 多平台 SQL 库,它的 Wasm 支持在 2024 年还是讨论阶段。你想在浏览器里用 Kotlin/Wasm 做点带本地存储的应用,能选的方案很有限:要么用 JS 的 IndexedDB 绑定(wrappers 社区有人做,但维护状态参差不齐),要么用 Wasm 的内存数据库(比如 SQLite 的 Wasm 编译版本,但跟 Kotlin 的绑定要自己写)。这跟 Kotlin/JVM 世界里 Spring Data、Exposed、jOOQ 的繁荣景象完全不能比。


甚至测试工具链都还在建设中。Kotlin 的多平台测试框架 kotlin.test 在 Wasm 目标上的支持是有的,但跟浏览器测试基础设施的整合很粗糙。你想在 CI 里跑 Kotlin/Wasm 的单元测试,得配置 Node.js 的 Wasm 运行时或者 headless Chrome,而测试报告的格式、覆盖率收集、这些周边工具的支持都还在早期。我看过一个 JetBrains 工程师在 KotlinConf 2024 上的演讲,提到他们内部测试 Kotlin/Wasm 编译器本身的时候,用的还是自定义的测试 harness,没有成熟的社区方案可用。


跟 Google 的 Wasm 布局的错位


Google 对 WebAssembly 的投入很大,但重心在 C++ 和 Rust 的迁移路径上。Chrome 团队的 Wasm 优化,很多是针对 Emscripten 编译的 C/C++ 代码,或者 Rust 的 wasm32-unknown-unknown 目标。V8 引擎的 TurboFan 对 Wasm 的编译优化,对 GC 语言的支持是后来才加进去的,而且优先级明显低于线性内存模型的语言。


Android 团队对 Kotlin/Wasm 的态度比较暧昧。官方博客里有几篇 Compose for Web 的 Wasm 迁移文章,但 Google I/O 2024 上 Wasm 相关的内容几乎没有 Kotlin 的戏份。Google 自己的 Wasm 应用案例,比如 Google Earth 的 Web 版本,用的是 C++ 编译的 Wasm,跟 Kotlin 没关系。这种"JetBrains 热推、Google 旁观"的格局,让 Kotlin/Wasm 的背书力度打了折扣。


一个更深层的问题是:Google 有没有动力让 Kotlin 在浏览器里太强?Android 是 Google 的移动生态核心,Kotlin 是 Android 的首选语言,这个绑定关系对 Google 是有利的。但如果 Kotlin 在 Web 端也建立起强大的存在,开发者用 Kotlin 写一套代码覆盖 Android 和 Web,那 Flutter(Dart)和 Angular(TypeScript)的位置往哪摆?Google 的内部产品矩阵有竞争关系,Kotlin/Wasm 的推进速度可能受到这种张力的间接影响。


JetBrains 当然想摆脱对 Google 的依赖,建立独立的 Kotlin 生态。但 Wasm 这个战场,没有浏览器的深度配合,没有 Google 级别的 runtime 优化投入,光靠 JetBrains 自己的编译器和框架团队,能走多远?Kotlin/Native 的 iOS 支持就是个参照:能跑,能用,但跟 Swift 的原生体验比,总是有层隔膜。


一个具体的性能对比案例


说点我实际测过的数据。用同一个简单的计算逻辑:生成 100 万个随机数,排序,求和。分别用 Kotlin/JVM(HotSpot 17)、Kotlin/JS(Node.js 20)、Kotlin/Wasm(Chrome 124)、纯 JavaScript(Node.js 20)实现。


结果大致是:Kotlin/JVM 最快,1.2 秒左右,HotSpot 的 JIT 对排序循环的优化很成熟。纯 JavaScript 第二,2.8 秒,V8 的 TurboFan 对数值数组的处理确实强。Kotlin/JS 第三,4.5 秒,Kotlin 编译出来的 JS 代码在数组访问上有额外的边界检查包装。Kotlin/Wasm 第四,5.1 秒,比 Kotlin/JS 还慢。


这个反直觉的结果,原因在 WasmGC 的实现成熟度。Chrome 124 的 WasmGC 支持虽然功能完整,但优化程度还不如 V8 对 JS 的多年打磨。Kotlin/Wasm 编译出来的代码用了 WasmGC 的 struct 类型来表示对象,但排序算法里频繁的对象字段访问,在当前的 WasmGC 引擎上没有走最优路径。JetBrains 的工程师在论坛里提到过,他们预期 WasmGC 的引擎优化会在未来几个 Chrome 版本里改善,但时间表不确定。


这个测试很不严谨,样本单一,环境受控,不能推广到所有场景。但它说明了一个问题:Wasm 的"理论性能优势"和"实际跑出来的性能"之间有 gap,而且这个 gap 的填补依赖于浏览器厂商的优先级,不是 JetBrains 能控制的。


那还值不值得跟进


我的个人判断是:Kotlin/Wasm 现在适合两种人。一种是 JetBrains 生态的深度绑定用户,已经在用 Compose Multiplatform 做跨平台应用,Web 端是不得不覆盖的目标,那 Wasm 是比 Kotlin/JS 更好的选择,尽管还有坑。另一种是技术好奇心驱动的早期采纳者,想提前占位,赌 WasmGC 的成熟和 Kotlin/Wasm 的生态完善。


对于大多数 Android 开发者,如果只是想扩展技能树到前端,TypeScript 仍然是更务实的路径。工具链成熟、工作机会多、社区问题有 Stack Overflow 答案。Kotlin/Wasm 的招聘信息在 2024 年几乎为零,你学了之后很难变现。


对于技术决策者,在团队里引入 Kotlin/Wasm 的风险目前大于收益。编译器还是 Alpha,库支持碎片化,调试工具原始,招聘市场上找不到有经验的工程师。除非你的产品形态极度依赖 Compose 的跨平台复用,或者有明显的计算密集型需求需要 Wasm 的性能隔离,否则不建议在生产环境押注。


JetBrains 的路线图里,Kotlin/Wasm 预计在 2025 年达到 Beta 或 Stable。但这个时间表是否可靠,取决于 WasmGC 在浏览器里的实际表现,以及 JetBrains 能否在 Native 和 Wasm 两个后端之间平衡资源。Kotlin/Native 的 iOS 支持从 Experimental 到 Production-ready 花了五年,Wasm 会不会重蹈覆辙?


最后的一点牢骚


技术选型里最让人疲惫的,不是学习新东西,而是判断"这个新东西会不会半死不活地拖着我"。Kotlin/Native 的早期用户应该懂这种感觉:编译慢、调试难、库少,但 JetBrains 的画饼一直画着,你弃之可惜,用之难受。Kotlin/Wasm 现在有点这个味道。


JetBrains 的宣传材料里喜欢强调"一次编写,到处运行"的多平台愿景。但真实的开发体验是"一次编写,到处调试"。你在 Android 上验证过的业务逻辑,搬到 Wasm 上可能因为线程模型差异死锁;你在 JVM 上跑通过的单元测试,在 Wasm 上可能因为内存布局差异 segfault。这些不是 Kotlin 语言的问题,是不同 runtime 的语义鸿沟。


Wasm 作为技术方向,我相信会成熟。但 Wasm 上的 Kotlin 能不能活得好,不取决于 Kotlin 语言本身的设计,而取决于 JetBrains 的持续投入力度、浏览器厂商的优化优先级、以及社区生态能不能突破冷启动。这三个变量里,至少有两个不在 JetBrains 的控制范围内。


现在去翻 Kotlin/Wasm 的 GitHub 仓库,contributor 列表里 JetBrains 的员工占绝对多数,外部贡献者的比例远低于 Kotlin 编译器的主线。这个生态活跃度,跟 Rust 的 wasm-bindgen 社区、或者 AssemblyScript 的社区比,差距明显。一门语言的目标平台,如果主要靠母公司输血,没有自发的社区造血,长期前景总要打问号。


Compose for Web 的 Wasm 版本,我每隔几个月会拉下来看看更新日志。修复的 bug 不少,新特性也在加,但那个内存泄漏的问题,我上次试的时候还在。可能下个版本就修好了,也可能还要等半年。这种不确定性,就是早期平台适配的真实代价。


JetBrains 在 KotlinConf 2024 上展示了 Kotlin/Wasm 跑 3D 图形 demo,用 WebGPU 渲染的。效果很炫,但代码量是同等 Three.js 实现的数倍,而且依赖的库都是 JetBrains 内部维护的实验项目。这种 demo 跟生产可用之间的距离,懂的都懂。


所以 Kotlin 的 Wasm 目标平台,到底能不能让 Kotlin 在浏览器里真正站稳脚跟?我的看法是:技术可行性已经证明,但工程成熟度和生态健康度还差得远。JetBrains 需要证明的不只是"能跑",而是"值得跑"——值得开发者押上时间和精力,值得公司押上产品和团队。这个证明过程,可能还要两到三年。


你现在会为了 Compose Multiplatform 的跨平台复用而提前上车 Kotlin/Wasm 吗?还是宁愿等它至少进 Beta 再说?这个选择本身,可能就是对 JetBrains 执行力的一次投票。

Material Design 3 的更新,视觉规范又变了 2026-06-15
Realm 数据库的现状,迁移到 Room 的经验 2026-06-15

评论区