3753 字
19 分钟
MoonBit 入门:用更小的 WASM 写安全的业务逻辑

TL;DR — MoonBit 是一门 WASM-first 语言,默认产物极小、冷启动快、类型系统强。适合边缘函数、浏览器插件、AI 插件沙箱等对加载速度敏感的场景;不适合依赖大量第三方库的服务端系统。

阅读时长约 8 分钟 · 适合正在评估 WASM 方案的工程师

版本说明:实测基于 moon 0.1.20260522 / moonc v0.9.3+b53c2807d(2026-05-26),正式选型前请用真实代码复测。


30 秒判断:MoonBit 适不适合你?#

你的场景建议
小而独立的 WASM 业务逻辑值得看 MoonBit
TypeScript 团队的简单 Wasm 工具先看 AssemblyScript;状态复杂时再评估 MoonBit
依赖大量库的服务端系统优先 Rust / Go / Java / TypeScript

MoonBit 是什么?#

由 IDEA 研究院(张宏波团队,BuckleScript/ReScript 核心作者)主导的为 WebAssembly 优化的多范式语言。核心定位:

关键词含义
WASM-first设计时就优先编译到 WebAssembly,不是事后适配
Minimal runtime尽量少把运行时塞进产物,.wasm 天然小
ML 系类型系统代数数据类型、模式匹配、类型推断、穷尽性检查
多后端同一份逻辑可面向 Wasm GC、传统 Wasm、JS、native

一句话:MoonBit 解决的是“一段业务逻辑,要安全、很小、很快地跑在沙箱里”。


为什么需要它?#

WASM 场景的难点往往不是“能不能跑”,而是“跑起来的成本够不够低”:

  • 边缘函数按请求启动,冷启动慢 = 延迟高
  • Chrome MV3 Service Worker 频繁被回收,产物越小恢复越快
  • AI 插件市场不希望下发大 runtime
  • 沙箱里的用户自定义逻辑,类型安全比“写起来最快”更重要

现有选择各有代价:

选择优点主要代价
Rust性能强、生态成熟、WASM 工具链完整学习曲线陡;默认产物不一定小,常要手动调优。
Go团队上手容易,服务端生态好标准 Go Wasm 会带较大的 runtime。
TinyGo明显缩小 Go Wasm 体积是独立工具链,语言和库支持不是完整 Go。
AssemblyScriptTypeScript 团队容易上手,微型 Wasm 很小不是普通 TS;复杂对象、内存、运行时选择需要额外理解。
MoonBit小体积、类型建模强、无所有权负担、多后端生态新,库和社区还在成长

MoonBit 的位置:比 Rust 更容易上手,比 Go 更轻,比 AssemblyScript 更适合复杂类型建模。


三分钟跑起来#

curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash
moon new hello && cd hello
moon run cmd/main

或者直接打开 在线 Playground。VS Code 用户建议装 MoonBit 官方插件


四个核心能力#

1. 产物极小#

MoonBit 的 release 构建和工具链设计都偏向小体积。原因不是“语法更短”,而是:

  • Minimal runtime — 不把用不到的运行时带进去
  • 整程序优化 — 没用到的函数和分配链路被消除
  • Wasm GC 后端 — 复用宿主 GC,不需要自带分配器
  • 专用优化管线 — 工具链从设计上就关注产物体积

结论:MoonBit 的体积优势主要体现在小型、独立、可沙箱化的业务逻辑里。(后面有实测数据。)

2. 编译快#

不依赖 LLVM,全链路自己实现 IR 与 codegen。增量编译很快,中等项目全量构建秒级。对“写一点、测一点”的插件和规则引擎很重要。

3. 强类型,无所有权心智负担#

ADT、模式匹配、Trait、Option / Result——适合表达协议、状态机、规则引擎。类型能力接近 OCaml / Rust,但不需要学生命周期和借用检查。

示例:二叉搜索树插入

enum Tree[T] {
  Leaf
  Node(Tree[T], T, Tree[T])
}

fn[T : Compare] Tree::insert(self : Tree[T], x : T) -> Tree[T] {
  match self {
    Leaf => Node(Leaf, x, Leaf)
    Node(l, v, r) =>
      match x.compare(v) {
        0 => self
        n if n < 0 => Node(l.insert(x), v, r)
        _ => Node(l, v, r.insert(x))
      }
  }
}

要点:Tree[T] 穷举所有形态;match 漏分支会编译失败;Compare 约束表达类型契约;函数体不需要到处写类型注解。

4. 多后端输出#

同一份逻辑可产出 .wasm.js 或 native 目标:

后端 / CLI target当前定位典型场景
Wasm GC / wasm-gc主推现代浏览器、新版 Node、支持 Wasm GC 的边缘 runtime
Wasm / wasm兼容路径传统线性内存 Wasm、嵌入式 Wasm、无 GC 环境
JavaScript / js可用前端、Node.js、不方便引入 Wasm 的环境
C / native可用但仍在演进CLI、服务端工具、需要本地可执行文件的场景
LLVM / llvm实验性后端实验、高性能原生目标探索

注意:WASM 与外部世界交互依赖宿主函数,I/O、网络等能力不能默认认为“直接可用”。wasm-gcuse-js-builtin-string 对宿主支持有要求,现代浏览器和新版 Node 支持较好,但边缘 runtime、嵌入式 runtime、组件模型宿主仍需单独验证。


实战示例:边缘函数 JSON 校验#

模拟一个 Cloudflare Workers / Vercel Edge handler:解析用户信息、校验、返回 Result

struct SignupReq {
  email : String
  age : Int
}

enum SignupError {
  InvalidEmail
  Underage(Int)
  MalformedJson(String)
}

fn[T] parse_json(_body : String) -> Result[T, String] {
  Err("invalid json")
}

fn validate(req : SignupReq) -> Result[SignupReq, SignupError] {
  if !req.email.contains("@") {
    Err(InvalidEmail)
  } else if req.age < 18 {
    Err(Underage(req.age))
  } else {
    Ok(req)
  }
}

pub fn handle_signup(body : String) -> Result[SignupReq, SignupError] {
  parse_json(body)
    .map_err(fn(e) { MalformedJson(e) })
    |> Result::bind(validate)
}

要点:SignupError 本身就是 API 文档;Result 强制调用方处理成功和失败;|> 把解析→映射→校验串成清晰管道。


横向对比:怎么选?#

语言WASM 大小启动类型安全学习曲线
Rust百字节 / KB 到 MB,取决于 std、分配、glue code 和调优较慢到快,取决于调优强 + 所有权
Go标准工具链通常 MB 级
TinyGo十几 KB 到几百 KB
AssemblyScript百字节级到几十 KBTS-like
MoonBit几 KB 到几十 KB,小函数可百字节级强(ADT + Trait)

快速决策:

  • 成熟生态和极致性能 → Rust
  • 复用 Go 团队经验 → TinyGo / 标准 Go Wasm
  • TypeScript 风格的小工具 → AssemblyScript
  • 复杂业务状态 + 小体积 + 强类型 + 多后端 → MoonBit

表中体积区间是工程经验值,不是语言常数。Rust 的无分配纯函数可以百字节级;一旦引入 std、分配、wasm-bindgen、serde 或其他 glue code,体积会快速上升。通过 no_std + wee_alloc + wasm-opt -Oz 等手段仍可压到很小。


为什么不直接用 AssemblyScript?#

如果目标是小型、无依赖、偏计算的 Wasm 工具且团队主要写 TypeScript,AssemblyScript 很可能更直接。但它不是”把现有 TS 直接编成 Wasm”——语法接近 TypeScript,工程边界更接近”用 TS 语法写的面向 Wasm 的静态语言”。

关键差异:

  • 运行时要自己选stub 是极小 bump allocator 不回收;minimal 需手动触发 GC;incremental 自动化但引入更多开销
  • 标准库不是浏览器/Node 的 — DOM、fetch、npm 包不会因为语法像 TS 就可用
  • 对象在线性内存里String/Array 不是 JS 对象,跨边界是指针
  • JS 互操作有成本 — 传字符串/对象需要 glue code / loader

结论: 简单 Wasm 工具、TS 团队 → AssemblyScript。业务状态复杂、错误分支多、长期维护 → 评估 MoonBit。


本机实测数据#

只想快速阅读可跳过本节。结论:微型纯函数里各家都能百字节级;稍真实的字符串工具里 MoonBit 和 AssemblyScript 仍控制在 1 KB 内;TinyGo 十几 KB;标准 Go 因 runtime 体积明显更大。吞吐方面,线性内存路径各家接近,wasm-gc + JS string 路径因逐字符宿主调用而明显慢。

ABI 说明:各产物的宿主绑定方式不同——MoonBit wasm-gc 使用 use-js-builtin-string,Go js/wasm 使用 syscall/js,其余主要通过 ptr + len 读取线性内存。表格主要用于观察产物量级和吞吐档位,不是严格 ABI 对比。

AssemblyScript 表中的 stub / minimal 是为小体积显式选择的 runtime;默认 runtime 和 GC 行为不同,体积也会变化。

测试 1:最小导出函数 validate_signup(age, email_has_at) -> 0|1|2#

不含 JSON、字符串解析或第三方依赖,只看最小可导出函数的产物下限(2026-05-27 实测):

工具链 / 产物Raw size说明
AssemblyScript 0.28.17 / stub84 Basc --runtime stub -O3z --noAssert
AssemblyScript 0.28.17 / minimal84 Basc --runtime minimal -O3z --noAssert
MoonBit moonc v0.9.3 / wasm-gc release93 Bmoon build --target wasm-gc --release --strip
MoonBit moonc v0.9.3 / wasm release102 B传统线性内存 Wasm target
Rust rustc 1.95.0 / release-small138 Bopt-level = "z" + LTO + strip + panic = "abort"
Rust rustc 1.95.0 / release437 B普通 cargo build --release
TinyGo 0.41.1 / WASI14,297 Btinygo build -target=wasi -opt=z -no-debug -panic=trap
TinyGo 0.41.1 / wasm15,312 Btinygo build -target=wasm -opt=z -no-debug -panic=trap
Go 1.26.3 / WASI reactor1,816,416 B//go:wasmexport + -buildmode=c-shared
Go 1.26.3 / js/wasm1,924,219 B标准 Go 浏览器 Wasm runtime 路径

微型 Wasm(<1 KB)压缩参考价值不大。所有产物已用 Node.js 验证通过。

测试 2:Markdown TOC Analyzer#

更接近真实工具:扫描 Markdown,识别 ATX 标题,统计数量、最大层级、做 ASCII lowercase checksum。所有产物验证结果:headings=6, max_level=6, checksum=3850

工具链 / 产物Raw sizeGzip -9Brotli -q 11说明
AssemblyScript 0.28.17 / stub537 B376 B324 Basc --runtime stub -O3z --noAssert
AssemblyScript 0.28.17 / minimal537 B379 B324 Basc --runtime minimal -O3z --noAssert
MoonBit moonc v0.9.3 / wasm-gc release681 B444 B390 Buse-js-builtin-string + moon build --target wasm-gc --release --strip
MoonBit moonc v0.9.3 / wasm linear memory971 B674 B593 Bptr + len 输入 + moon build --target wasm --release --strip
Rust rustc 1.95.0 / release-small3,326 B1,737 B1,534 Bopt-level = "z" + LTO + strip + panic = "abort"
TinyGo 0.41.1 / WASI15,045 B6,843 B5,941 Btinygo build -target=wasi -opt=z -no-debug -panic=trap
TinyGo 0.41.1 / wasm16,081 B7,054 B6,139 Btinygo build -target=wasm -opt=z -no-debug -panic=trap
Rust rustc 1.95.0 / release21,908 B8,808 B7,558 B普通 cargo build --release
Go 1.26.3 / WASI reactor1,819,505 B544,898 B421,680 B//go:wasmexport + -buildmode=c-shared
Go 1.26.3 / js/wasm1,926,413 B571,813 B440,637 B标准 Go 浏览器 Wasm runtime 路径

结论:MoonBit 和 AssemblyScript 接近或低于 1 KB,Rust 瘦身后几 KB,TinyGo 十几 KB,标准 Go 受 runtime 主导在 MB 级。

测试 3:Markdown TOC Analyzer warm throughput#

同一份 Markdown analyzer 的运行时吞吐:输入重复 16 次(6,352 B),Node.js v24.6.0 先 warmup 150 ms,再取 5 轮、每轮 500 ms 的 median ops/sec。

产物Median ops/secMedian us/op相对 MoonBit linear
Rust release128,6357.771.13x
Rust release-small123,9678.071.09x
AssemblyScript minimal116,7148.571.03x
AssemblyScript stub116,6448.571.03x
TinyGo wasm114,0608.771.01x
MoonBit wasm linear memory113,4718.811.00x
TinyGo wasi113,1668.841.00x
Go WASI reactor44,69622.370.39x
Go js/wasm25,44239.310.22x
MoonBit wasm-gc + JS builtin string14,59168.530.13x

结论:线性内存路径各家接近(MoonBit ≈ AssemblyScript ≈ TinyGo),Rust 快约 13%。wasm-gc + JS string 路径因逐字符宿主调用而慢 ~7x——吞吐敏感场景应优先用线性内存 ABI。

测试 4:官方 FFT benchmark 复测#

MoonBit 官方博客用 Fast Fourier Transform 展示 #valtype 对数值计算的性能价值,并给出了 moonbit-community/benchmark-fft 仓库。该仓库 README 记录的 MoonBit 环境是 v0.6.25;当前本机是 moonc v0.9.3+b53c2807d,原始 MoonBit 代码不能直接编译,因此复测时只做兼容迁移:更新 Add/Sub/Mul trait 方法名,移除旧版 moonbitlang/x / tonyfettes/uv / fs 依赖,计时改用 moonbitlang/core/bench

命令:python3 bench_runner.py --runs 10 --inputs 18 20 22。输入 18/20/22 分别表示 2^18 / 2^20 / 2^22 个 complex points。

Program2^18 Median2^20 Median2^22 Median
MoonBit native15.790 ms64.759 ms300.750 ms
Go15.755 ms64.727 ms300.923 ms
Rust24.176 ms98.435 ms428.383 ms
Swift37.286 ms155.591 ms699.491 ms

结论:MoonBit native 与 Go 同档,比 Rust 快约 30%、比 Swift 快约 57%(2^22)。FFT 考验 native 数值计算和 value type,与 Markdown 测试暴露的宿主 ABI 成本是不同维度。


适合的场景#

场景为什么适合主要替代方案
边缘函数的计算核心产物小,冷启动快,沙盒安全。Rust、JS/TS、AssemblyScript
浏览器插件 / Chrome MV3 Service WorkerMV3 SW 频繁被回收,重新启动时体积和初始化成本很关键。Rust、TS
小程序 / 快应用的 WASM 业务逻辑产物大小和冷启动直接影响体验。AssemblyScript、C/C++、Rust
IoT 嵌入式设备的 WASM 沙箱业务小体积、沙箱隔离,native / LLVM 路径也值得跟踪。C/C++、Rust、MicroPython
AI 助手的用户自定义插件 / DSL 沙箱WASM 天然沙箱,类型安全方便审计,小体积方便下发。QuickJS、V8 isolate、用户手写 JS
前端 Monorepo 的共享业务逻辑同一份逻辑可以面向 JS、Wasm、Node、浏览器扩展。TypeScript、Rust
游戏规则 / 实时渲染的轻量业务逻辑ADT + 模式匹配适合表达规则和状态机。Lua、Rust、AssemblyScript
WASM 组件模型相关的小组件类型系统贴合组件类型契约,体积小适合分发;WIT / ABI 工具链成熟度需单独验证。Rust、C/C++

一句话:凡是对 WASM 产物大小、冷启动速度、沙盒安全敏感,且业务逻辑能清晰隔离的场景,都值得评估 MoonBit。

不适合的场景#

  • 依赖海量第三方库 — 生态不能和 Rust/Go/JS 相比
  • 纯服务端 / 不在乎冷启动 — 常驻进程、批处理用成熟技术更稳
  • 需要稳定 ABI / FFI — native 与 LLVM 路径还在演进
  • 高吞吐字符串 / 字节扫描热路径 — 需要先做性能 POC,并优先验证线性内存或组件模型边界;不要默认使用 JS string 热循环
  • 大团队招人 / 长期维护核心系统 — MoonBit 工程师少
  • 强调成熟稳定高于一切 — 语言和工具链仍在快速演进
  • 修改/分发编译器 — MoonBit Public License 有限制,需先确认条款

决策清单#

问题答“是”意味着
逻辑能独立成小模块?MoonBit 值得看
产物大小/冷启动影响体验或成本?MoonBit 值得看
业务状态和错误分支越来越复杂?MoonBit 类型系统有帮助
热路径主要是大文本 / 字节扫描?先测 ABI 和宿主边界,不要只看 Wasm 体积
严重依赖成熟第三方库?先别急着上 MoonBit
需要大量招聘和长期稳定生态?谨慎评估,可能先观望

MoonBit 不是来替代 Rust 或 JavaScript 的。它是为“WASM-first、小体积、快冷启动、强类型沙箱逻辑”准备的专用工具。


附录:为什么 Rust WASM 比 MoonBit 大?#

不是”Rust 天生慢”,而是几个默认设计决策叠加的结果:

因素Rust 默认行为MoonBit 对应策略
std/runtime胖 std 静态链接,println! 就可能引入格式化引擎、分配器链、Unicode、Panic 处理minimal runtime + 整程序优化,未用函数被消除
内存分配器使用 String/Vec/Box 等堆分配路径就需要带分配器进 WASMWasm GC 后端复用宿主 GC,不自带分配器
优化后端LLVM 通用后端,内联保守,泛型单态化可能重复生成代码,需手动接 wasm-opt自写 IR + 优化管线,专为 WASM 体积特化

Rust 能变小,但门槛不低: #![no_std]opt-level = “z” + LTO + strip → panic = “abort”wee_allocwasm-opt -Oz → 优化 wasm-bindgen。调完可以和 MoonBit 一样小,代价是放弃标准库生态。

Rust:”默认保留完整能力,Wasm 体积需要额外调优”。 MoonBit:”默认极简、默认 WASM 特化、冷启动/体积第一”。 没有对错,看你的场景更需要成熟生态还是小体积。


参考资源#

本文中的体积和冷启动判断,建议结合官方 benchmark 与真实业务代码复现。

MoonBit 入门:用更小的 WASM 写安全的业务逻辑
https://wsafight.github.io/personBlog/posts/moonbit/
作者
wsafight
发布于
2026-05-27
许可协议
CC BY-NC-SA 4.0