NetWasm 是面向 WebAssembly 与 WASI 的独立 .NET 方案,提供编译器、CoreLib、运行时和 SDK,可将 C# 和 .NET CIL 直接编译成 WebAssembly,且不把桌面 CLR、Mono、dotnet.js 或应用 DLL 带入生产,用于编写 C# 并部署小型独立 Wasm。相同 print-42 工作负载下,NetWasm 加精确 GC 为 47,895 字节,结果含运行时与精确 Compact 垃圾回收器;Rust 1.95.0 的 size-oriented profile 为 53,635 字节。两者均为独立 WASI Preview 2 Components,NetWasm 原始体积小 10.7%,Brotli-11 压缩后小 19.5%。
正文
NetWasm 是面向 WebAssembly 和 WASI 的独立 .NET,可以用 C# 编写并部署小巧、独立的 Wasm。它是一套独立的编译器、CoreLib、运行时和 SDK,能把 C# 和 .NET CIL 直接编译成 WebAssembly,而不会把桌面 CLR、Mono、dotnet.js 或应用程序 DLL 带进生产环境。
C# 在 Wasm 体积上现已击败 Rust
加上精确 GC 的 NetWasm 为 47,895 字节;采用面向体积配置的 Rust 1.95.0 为 53,635 字节。对于同样打印 42 的工作负载,NetWasm 的结果已经包含运行时和精确的 Compact 垃圾回收器。两者都是独立的 WASI Preview 2 Component,按开发者开箱即得的方式构建:各自工具链的标准库和普通构建,没有手动添加工具。
原始体积小 10.7%,用 Brotli-11 压缩后小 19.5%。
| 工具链 | 原始 Wasm | Brotli 11 | 对比角色 | | --- | --- | --- | --- | | NetWasm,Compact GC | 47,895 B | 13,660 B | WASI P2 Component;包含精确 GC | | Rust 1.95.0,体积配置 | 53,635 B | 16,963 B | 匹配的 WASI P2 Component 参照 | | Kotlin 2.4.0 wasmWasi | 79,293 B | 22,655 B | P1 环境;GC 由宿主引擎提供 | | TinyGo 0.39.0 | 93,043 B | 26,931 B | P1 环境;配置精确 GC |
Rust 是主要对比对象,因为它使用相同的工作负载和输出形式。Kotlin/Wasm 和 TinyGo 展示了更广泛的体积情况,但它们的目标和运行时边界不同。这是一项在特定工作负载和配置下测得的结果,不是全语言范围的体积排名。
NetWasm 的数字来自模板应用程序默认的 Release 发布。Rust 则在普通 cargo build 之外,加上了 Cargo 文档中的体积设置(opt-level = "z"、LTO、单个 codegen unit、panic = "abort" 以及剥离符号)。Binaryen 的 wasm-opt 在这里不是后处理步骤:它是 NetWasm 编译器流水线的一部分,在每次普通构建中运行,无需安装或启用任何东西。Rust 的 wasm32-wasip2 输出是一个 component,因此对它应用 wasm-opt 意味着要提取核心模块、优化它,再手工重新编码 component。
两种语言都能变得更小,但那就属于另一类程序了:Rust 用 no_std,或 C# 用 bflat 的 zero library 这类最小库,都会放弃大部分标准库;对 C# 来说,还要放弃垃圾回收器。
比 Mono AOT 更快
NetWasm 运行分配和垃圾回收基准测试时,比由相同 C# 源码编译、并在相同 Wasmtime 中运行的 Mono AOT 更快:
| 工作负载 | NetWasm,NetWasmGC | Mono AOT,.NET 10 | NetWasm 耗时 | | --- | --- | --- | --- | | 完整事务 | 43.9 ms | 59.1 ms | 少 26% | | 保留对象图 | 8.8 ms | 14.2 ms | 少 38% | | UI 抖动 | 17.6 ms | 29.1 ms | 少 40% |
这些是在 Apple M1 Max、Wasmtime 49.0.3 下运行 15 次的中位数,每次运行都与桌面 .NET 的结果进行核对。Mono AOT 是 Microsoft 的 wasi-wasm 目标,采用默认 Release 配置。在前两个工作负载上,NetWasmGC 也比桌面 .NET 11 的 JIT 更快(49.9 和 9.7 ms);在 UI 抖动上 JIT 更快(8.6 ms),而桌面 NativeAOT 在三项上都最快。
不止 Hello World
Hello World 只是最小的例子,NetWasm 也能构建真实应用。它支持 C# 15、JavaScript 模块与 Web Worker、静态 .a 互操作、WIT/WASI P2、表达式树、HTTP/JSON/XML/Regex、免反射 DI、TUnit/VSTest、wasm64,以及三种分配策略。
如今 NetWasm 可以:
- 通过普通的 dotnet CLI 构建、运行、发布和测试项目;
- 将 C# 导出发布为在页面或 Web Worker 中运行的类型化 JavaScript 模块;
- 链接兼容的 Wasm .a 归档,并生成有界的 C ABI 编组;
- 从 WIT 生成 C# 绑定,并产生独立的 WASI P2 Component;
- 在不需要通用运行时反射的情况下构造和检查表达式树;
- 使用独立打包的 LINQ、HTTP、JSON、XML、Regex、哈希和依赖注入库;
- 生成面向 netwasm0.1 和桌面 .NET 的双目标库。
NetWasm 的平台契约刻意比桌面 .NET 小。它已经支持完整应用程序、可移植库、测试项目和浏览器模块。
试一试
安装 .NET SDK 10.0.303 或更高版本,然后运行:
- dotnet new install NetWasm.Templates
- dotnet new netwasm-app -n HelloNetWasm
- cd HelloNetWasm
- dotnet restore
- dotnet run
- dotnet publish -c Release -o publish/local
用兼容 WASI Preview 2 的宿主(如 Wasmtime)运行发布的组件:
wasmtime run publish/local/HelloNetWasm.wasm
运行后输出 42。发布的组件在 .NET 方面是自包含的,但仍然需要 WebAssembly 宿主。用 .NET 构建,无需 .NET 即可运行。
使用 C# 15 的项目选择 .NET 11 SDK;较早的语言版本可以留在 .NET 10。SDK 通过 NuGet 还原其固定的 Node、LLD 和 Binaryen 工具,因此普通应用不需要单独安装 Emscripten、Git 或 Python。
它如何保持小巧
Roslyn 生成 CIL。NetWasm 把可达程序直接编译成 Wasm,对其泛型进行特化,并只链接所需的运行时支持。链接后的 Wasm component 就是程序本身。
封闭世界编译。不可达代码、导入和运行时特性不会进入产物。
更小的平台契约。NetWasm 支持可移植 API,而不是复现桌面运行时的每一个假设。
没有宽泛的运行时反射。运行时类型名、动态程序集加载和 dynamic 关键字都在该契约之外。
生成的边界。JSON、DI、WIT 和原生互操作使用已知契约,而不是运行时发现。
可选数据和诊断。时区数据、丰富堆栈跟踪和其他部署数据仅在选择时才包含。
单一回收器。只链接所选的分配策略。
这是一套新的 .NET 平台,而不是被塞进 Wasm 沙箱的桌面 .NET。正是这一边界让原生 Wasm 体积的 C# 成为可能。
JSON 和可选功能如何影响体积
选择分配策略
Compact 是默认策略:一种体积优先、精确、非移动的标记-清除回收器。它跟踪编译器发布的根和引用布局,无需保守扫描。它的名字指的是实现体积,而不是移动堆压缩。
| 策略 | 最佳适用 | 原始组件 | Brotli 11 | | --- | --- | --- | --- | | Compact(默认) | 小型应用和低变动 UI | 47,895 B | 13,660 B | | NetWasmGC | 更高的分配吞吐 | 50,855 B | 15,053 B | | Bump | 有界的一次性任务 | 43,864 B | 11,959 B |
在可执行项目的 .csproj 中设置 NetWasmGarbageCollector 属性(例如设为 NetWasm);不设置则选择 Compact。库保持回收器中立;wasm32 和 wasm64 都打包了全部四个后端。
库应当向上兼容
为更小的 NetWasm 契约编写的库,也应该能在桌面 .NET 上有用,而不是被困在仅限 Wasm 的生态里。可通过 dotnet new netwasm-lib -n MyLibrary 创建库。
该模板面向 netwasm0.1 和 net10.0,为两个平台生成 NuGet 资产。这是显式的多目标,而不是承诺任意桌面包都能反向工作。
NetWasm.Libraries 提供移植的框架和扩展包。TUnit-NetWasm 通过通用 VSTest 桥接提供普通的 dotnet test 集成。
与常规 .NET Wasm 相比如何?
对于一个打印 42 的空 UI Blazor WebAssembly 应用(启用 AOT、托管裁剪、IL 剥离、固定全球化和面向体积的原生链接):
| AOT 构建 | 未压缩 Wasm | 相对 NetWasm | | --- | --- | --- | | NetWasm,Compact GC | 47,895 B | 1× | | Blazor WebAssembly,.NET 10 | 12,276,684 B | 256× | | Blazor WebAssembly,.NET 11 RC1 | 15,289,241 B | 319× |
Blazor 的总量包含其原生运行时和所需的 Webcil 程序集;NetWasm 包含其运行时、垃圾回收器和 WASI 组件。这是控制台组件与空 UI 应用的对比,不是等效框架的对比,也不声称适用于所有 .NET 工作负载。
当前边界
NetWasm 处于 1.0 之前。其受支持契约现在可用,并有已发布的合格认证覆盖,但源码、包、目标契约和 ABI 契约可能在次要版本之间变化。
正常的可移植输出是 wasm32 WASI Preview 2 Component。原始 wasm32、原始 wasm64 和 JavaScript 宿主集成是高级替代方案;wasm64 Component 仍受上游工具阻塞。
运行时反射、dynamic、运行时程序集加载和托管线程都在契约之外。通用的 System.IO.File / Directory / FileStream、原始套接字和子进程 API 尚未实现。现有 NuGet 包只有在其可达代码和 API 依赖适合受支持契约时才能工作。
通往 1.0 之路
NetWasm 1.0 意味着一个有用、有文档且稳定的更小平台,而不是“桌面 .NET,但在 Wasm 里”。主要剩余主题包括:
- 更广的 WASI Preview 2 覆盖;
- 全球化模式和可选文化数据;
- 表达式树完成;
- 更快的增量开发循环;
- 稳定的 netwasm1.0 平台和运行时契约;
- 生产诊断和更广的宿主合格认证。
NetWasm 0.10.0 新增:重建的垃圾回收器。对象不带回收器头,数组是单次分配,NetWasmGC 通过推进指针分配,Compact 拥有为体积和内存打造的新核心。分配密集的程序运行更快、占用内存更少,发布时区数据的应用启动时间也只是原来的一小部分。Boehm 回收器已移除。
正在进行:提升与现有 .NET Standard 2.1 库二进制和 NuGet 资产的兼容性。目标是在其可达代码符合 NetWasm 契约时原样使用这些库,而不会把未使用的不受支持路径带进应用。
这不会把 NetWasm 变成桌面 .NET。可达的通用运行时反射、dynamic、运行时程序集加载、托管线程、不受支持的宿主 API 和具名文化全球化仍在初始兼容范围之外,并会在编译时失败。
许可证
CoreLib、运行时库、模板和生成的支持代码为 MIT,须保留上游声明。编译器和开发者工具使用 NetWasm Community License 1.0。Compact 和 NetWasmGC 回收器为 Apache-2.0。
Community License 对符合条件的个人、教育、开源工作、评估,以及员工少于 250 人且年收入低于 1000 万美元的组织免费。匹配的 GitHub Sponsors 层级授予有限的内部商业使用;OEM 和商业产品化使用需要单独协议。生成的应用可以使用作者选择的许可证。没有登录、激活、遥测或技术强制措施。
评论
多数评论在质疑这个“C# 击败 Rust”的 WASM 体积对比:认为 C# 一侧做了后处理、捆绑运行时和 GC,且只比 hello world,Rust 经同等优化可以小得多,因此标题至少是误导。少数人为该工具或 C# 的后端体验叫好,也有人从根本上否定 WASM 在普通 Web 开发中的价值,并提醒该方案并不开源。
认为“C# 击败 Rust”的对比不公平、标题误导
- C# 的基准代码经过了额外后处理,而 Rust 等其他目标并没有;Rust 若做同等优化可以小得多。C# 版本还随运行时和 GC 一起分发,比较只停留在 hello world 这种极窄场景,因此“击败 Rust”远非事实(高赞) > 这说法最好听是误导,最难听是谎言;工具虽棒,但除了展示它的狭窄基准外,远不能说击败了 Rust(@sondr3)
- 参与比较的 C# 版本把运行时和垃圾回收一起打包进去,而 Rust 自己管理内存,二者前提并不相同;有人冷嘲这不过是把膨胀转嫁给了 WASM 运行时自身(少数人)
认为 47KB 的 hello world 并不值得兴奋且比较无关紧要
- 即便不看对比是否公平,为一个只输出 42 的打印程序付出 47KB 也谈不上突破;而当被比较的对象本身无关紧要、差距又很边际时,这种“X 优于 Y”的观察价值很低(较多人) > 为一个只打印 42 的程序要 47KB,我实在兴奋不起来(@edave64)
认为 WASM 在普通 Web 开发中并不值得投入
- 除了 Web 版 AutoCAD、Premiere 这类重性能应用,WASM 没什么吸引力;有团队用 Blazor Server 五年从未考虑切到 WASM 模式,与其折腾 WASM 管道和 WebSocket 传输,不如写传统的 cshtml 加 JavaScript 跑在无状态 HTTP 上(少数人) > 除非你在做网页版 AutoCAD 或 Premiere,否则我看不出用这个技术的意义(@bob1029)
肯定该工具或 C# 本身的开发体验
- 有人觉得这个工具看起来非常出色,想立刻尝试;也有人说只要遵循较新的写法,C# 用来写后端非常顺手,工具链、现代语言特性(如 C# 15 的 unions、closed hierarchies)和调试体验都很好(较多人) > 只要坚持较新的约定,C# 做后端就是很对味:工具链、语言特性和调试体验都很棒(@CharlieDigital)
提醒该工具并非开源,会影响对比和选型
- 被拿来和 Rust 比较的这个方案并不是开源的;对把开源作为重要考量的人来说,这是必须纳入对比的因素(少数人) > 值得指出的是,它并不是开源的;如果在比较两者时你在意这一点,那就很重要(@Scaled)
认为 C# 会慢慢失去重要性、主要靠 .NET 存量被选用
- C# 正在慢慢边缘化,如今被选用往往只是因为公司里已经有大量 .NET 代码(少数人) > C# 会慢慢褪去重要性,现在被选中主要是因为公司已有大量 .NET 家底(@VimEscapeArtist)
有价值的补充
- Rust 编译出的 WASM 二进制本来就十分庞大;若关注 WASM 的性能、体积与内存占用,Zig 才是更值得考虑的候选(少数人)