NetWasm: C# just beat Rust at Wasm size.
NetWasm is an independent .NET compiler, CoreLib, runtime, and SDK that compiles C# and .NET CIL into small, standalone WebAssembly modules and WASI Preview 2 components.
Write C#. Deploy like C++. Target WebAssembly and WASI.
C# with precise garbage collection: 47,798 bytes. Rust's size-oriented profile: 53,635 bytes.
For the same print-42 workload, the measured NetWasm 0.8.0 release is
10.9% smaller, with its runtime and precise Compact GC included.
Both outputs are standalone WASI Preview 2 components. The Rust reference uses
Rust 1.95.0 with opt-level = "z", LTO and the size settings documented in
the methodology.
This is a measured workload/profile result, not a language-wide size guarantee.
These measurements were reproduced using the released NetWasm 0.8.0 SDK packages.
NetWasm compiles C# into a standalone WebAssembly Component, without carrying the desktop .NET runtime. Keep the language, generics, exceptions and managed memory. Deploy the compiled program to a compatible WASI host.
Compile C# to WebAssembly · Try it in the browser · Measured size and reproduction steps · Quickstart · JavaScript interop · Static C interop · WIT bindings · Garbage collector choices · Supported APIs and limitations
AOT vs AOT: printing 42
46.7 KiB with NetWasm. 14.58 MiB with .NET 11 Blazor WebAssembly AOT. Using the exact byte counts, NetWasm is about 320× smaller.
We published an empty-UI Blazor WebAssembly app that calls
Console.WriteLine(42), with AOT, full managed trimming, IL stripping, invariant
globalization and size-focused native compilation/linking enabled. Both the
.NET 10 and .NET 11 builds printed 42 in Chromium without errors.
| AOT build | Uncompressed Wasm | Size relative to NetWasm |
|---|---|---|
| NetWasm 0.8.0, Compact GC | 47,798 bytes (46.7 KiB) | 1x |
| Blazor WebAssembly, .NET 10 | 12,276,684 bytes (11.71 MiB) | 257x |
| Blazor WebAssembly, .NET 11 RC1 | 15,289,241 bytes (14.58 MiB) | 320x |
These are measured Wasm payloads, with no compression and no JavaScript counted. Blazor's total includes its native runtime and required Webcil assemblies; the native runtime alone was 9.04 MiB on .NET 10 and 11.18 MiB on .NET 11 RC1. NetWasm's figure includes its runtime, garbage collector and WASI component. NetWasm does not require a UI framework to run your C#. This compares a console component with an empty-UI Blazor application, not equivalent UI frameworks or a minimum size for every .NET application.
Exact versions, settings and reproduction.
Try it
Install .NET SDK 10.0.303 or newer, then follow the host and restore notes. Earlier .NET 10 SDK patches carry an ARM64 runtime race that can crash parallel compilation. Projects using C# 15 select the .NET 11 SDK; projects using earlier language versions can stay on .NET 10, and neither path requires both SDKs. The SDK restores its pinned Node, LLD and Binaryen tools for your development host through NuGet. You do not need to install Emscripten, Git or Python to build an ordinary app or test project.
Install, create, run and publish:
dotnet new install NetWasm.Templates
dotnet new netwasm-app -n HelloNetWasm
cd HelloNetWasm
dotnet restore
dotnet run
dotnet publish -c Release -o publish/localRun the published component directly with a compatible WASI Preview 2 host. For example, with Wasmtime:
wasmtime run publish/local/HelloNetWasm.wasm
42
The component is self-contained with respect to .NET: no CLR, Mono,
dotnet.js, application DLLs, or NetWasm installation is required at runtime.
A WebAssembly host is still required. Build with .NET; run without .NET.
For Windows host support, browser publishing and dual-target libraries, follow the complete SDK quickstart.
More than Hello World
The 46.7 KiB program is the baseline, not the boundary. NetWasm already covers a practical .NET development loop:
- C# 15 when the project selects the .NET 11 SDK, while the default template remains on .NET 10;
dotnet build,dotnet run,dotnet publishanddotnet test;- C# Web Workers with generated JavaScript clients and either a portable WIT or direct JSExport boundary;
- TUnit-NetWasm tests through the generic VSTest bridge and ordinary
dotnet test; - independently packaged ports for LINQ, HTTP, JSON, XML, Regex, Hashing and reflection-free dependency injection;
- dual-target library projects for
netwasm0.1and desktopnet10.0.
The supported surface is deliberately smaller than desktop .NET, but it is already broad enough to build, test and package non-trivial programs.
.NET designed around Wasm
I started NetWasm with a simple challenge: “Kotlin has it, why can’t we?!”
“It” is Kotlin Multiplatform (KMP)'s common API surface: shared code uses portable APIs instead of the whole JVM/Java library surface, with Wasm among its targets. Think of a smaller platform contract, in the spirit of .NET Standard, not a desktop runtime squeezed into every deployment.
C# does not require the desktop .NET runtime's entire compatibility surface. NetWasm starts with a different boundary: a smaller CoreLib, closed-world compilation, precise garbage collection and explicit WASI interfaces.
Roslyn produces CIL. NetWasm compiles the reachable program directly to Wasm,
specializes its generics, and links only the runtime support it needs. It does
not ship a CLR, Mono, dotnet.js, or application assemblies. The linked Wasm
component is the program.
This is a new .NET platform, not an attempt to make every desktop assumption work inside a Wasm sandbox. That tradeoff is what makes the small artifact possible.
Pay for what you use
- Reachable code and imports only. An unused package reference is not a reason to retain its implementation or request its host capabilities.
- No runtime type names. Runtime type-name lookup and broad reflection are unsupported, so namespace-qualified type-name metadata never enters the Wasm. Application strings, JSON property names, and names required by public interfaces are ordinary reachable data.
- JSON has tiers, not one fixed runtime tax.
JsonDocumentread-only DOM parsing, source-generated serialization and typed deserialization retain different closures. See the scenario measurements. - Timezone data stays out of the Wasm. UTC needs no timezone database; local-time support uses an explicitly selected deployment sidecar.
- Debugging is a build choice. Debug enables managed stack traces; Release omits their instrumentation and symbol sidecar unless requested.
- Only the selected collector is linked. Compact is the default from 0.7.0; choosing Boehm does not make libraries collector-specific or link both backends.
The 47,798-byte figure is the uncompressed final component, not a compressed download or the size of a complete JavaScript-host deployment. A WASI host is still required. The measurement notes make that boundary explicit.
Small by default. Choose your allocation strategy.
Compact is a size-first, precise, non-moving mark-and-sweep collector. It traces compiler-published roots and reference layouts without conservative scanning. It supports NetWasm's managed lifetime features, including finalizers, resurrection, weak references, dependent handles and pinning. Its name refers to implementation size, not moving heap compaction.
NetWasmGC is the throughput-oriented precise, non-moving collector. Select
NetWasm for faster allocation and collection paths at the cost of more memory
and a larger module. Bump is a non-collecting option for bounded, one-shot jobs.
Boehm remains available. Benchmark your application before choosing a collector.
For the same 0.8.0 candidate Release/Oz Console.WriteLine(42) component:
| Collector | Uncompressed final component | Brotli quality 11 |
|---|---|---|
| Compact (default) | 47,798 bytes | 13,501 bytes |
| NetWasmGC | 51,300 bytes | 15,157 bytes |
| Bump | 43,794 bytes | 11,929 bytes |
| Boehm | 74,142 bytes | 25,023 bytes |
To select NetWasmGC, put this in the executable project's .csproj:
<PropertyGroup> <NetWasmGarbageCollector>NetWasm</NetWasmGarbageCollector> </PropertyGroup>
Leaving the property unset selects Compact. This is a build-time application choice; class libraries remain collector-neutral. All four backends are packaged for wasm32 and wasm64. See Choosing a garbage collector for the tradeoffs, selection rules and diagnostics.
Libraries should travel upward
A library written for the smaller NetWasm profile should also be useful on desktop .NET, not trapped in a Wasm-only ecosystem.
dotnet new netwasm-lib -n MyLibrary
The template targets both netwasm0.1 and net10.0, producing NuGet
library assets for each platform. Portability is explicit multi-targeting,
not a promise that an arbitrary desktop package will work in reverse.
The ported libraries are
independent packages, including LINQ, HTTP,
JSON, XML, Regex, Hashing and reflection-free dependency injection.
TUnit-NetWasm uses the same public
SDK and generic VSTest bridge for dotnet test integration.
Packages
Ported-library packages · TUnit packages
The direction
Reflection-free .NET. WASI as the new POSIX.
The aim is a portable C# ecosystem built around ahead-of-time compilation, explicit interfaces and source generation rather than runtime discovery. WASI supplies a language-neutral platform boundary; C# should participate without carrying a desktop runtime everywhere it goes.
That is the direction, not a claim that today's profile implements all of .NET or all of WASI.
Roadmap to 1.0
NetWasm 1.0 does not mean “desktop .NET, but in Wasm.” It means the smaller platform has a useful, documented boundary that applications can depend on.
These are the release gates:
| Area | The v1.0 bar |
|---|---|
| Practical WASI Preview 2 | Ordinary C# can use files and directories, streams and polling, CLI arguments and environment, clocks, randomness, outgoing and incoming HTTP, and TCP/UDP/DNS where the host grants those capabilities. Browser and standalone adapters publish the capability pairs they support, with consistent lifetime/error semantics and clear failure for unavailable operations. WebSockets use an explicit versioned host contract because WASI P2 has no standard WebSocket interface. Internal completion-oriented boundaries keep a later P3 migration out of application APIs; v1.0 does not wait for P3. |
| Static native interoperability | Applications can link compatible Wasm .a archives, use source-generated LibraryImport plus a bounded raw DllImport C ABI, and expose supported UnmanagedCallersOnly callbacks. Archive dependencies, relocations, memory ownership, callbacks, GC safety and failure diagnostics are deterministic and pay-as-you-use. Dynamic libraries, runtime symbol lookup, native threads and a direct C++ ABI remain outside the profile. |
| Globalization and resources | Invariant, selected-culture and full-data modes provide documented culture-aware formatting and parsing, casing, comparison, Unicode normalization and properties, calendars, resource fallback and timezone behaviour. Versioned data stays optional so an invariant application does not carry it. Ordinary applications do not need native ICU installed on the build or deployment machine. |
| Expression trees | Applications can construct and inspect System.Linq.Expressions trees without requiring general runtime reflection. Expression.Compile() has an interpreted implementation for trees created at runtime, with an optional build-time generated fast path for expressions known during compilation. Neither path depends on runtime IL generation or a JIT. |
| Compiler and runtime correctness | The supported CIL and library profile has no known correctness bugs. GC, generics, variance and casts, interface and virtual dispatch, exceptions, async continuations, static initialization, callbacks and interop are covered by retained regression and compatibility suites. Builds are deterministic, and CLI and browser pipelines produce the same bytes from the same inputs. |
| Fast development loop | Unchanged and reverted inputs reuse persistent compiler, runtime-link and component artifacts. An ordinary method edit avoids replaying unaffected whole-program work. Release builds keep full optimization and validation without making every edit pay publish-time costs. |
| Stable platform contract | NetWasm 1.0 introduces the netwasm1.0 target profile; netwasm0.1 remains the 0.x profile instead of changing in place. The target profile, runtime ABI, host capability contracts, package compatibility rules and diagnostics have an explicit 1.x compatibility policy. Later target-profile versions change for platform-contract breaks, not merely because a package was released. Deprecations and unsupported operations fail clearly instead of silently changing behaviour. |
| Diagnostics and production delivery | Managed stack traces, deterministic Debug output, trap/crash symbolization and actionable compiler/linker diagnostics cover supported deployments. Restore, build, test and publish work from packages on macOS ARM64, Linux ARM64/x64 and Windows ARM64/x64. Wasmtime plus Chromium, Firefox and WebKit release gates cover the portable component and browser paths. |
| Ecosystem and supply chain | The reference pack and ported libraries publish a generated member-level support catalogue. Templates, TUnit/VSTest, WIT bindings and the supported libraries work as one qualified package graph. Toolchains are pinned, release artifacts are reproducible and attributable, and capability/resource boundaries have focused hostile-input tests. |
The roadmap deliberately excludes runtime reflection, dynamic, runtime
assembly loading, managed threads and a promise that arbitrary desktop NuGet
packages work unchanged. wasm64 core modules remain useful, but wasm64
Components cannot become a v1.0 gate until the upstream Component Model tools
and hosts support them end to end.
The detailed support inventory remains the source of truth for what is implemented today. A roadmap item is not a current support claim.
Current boundary
NetWasm is pre-1.0. The supported profile is usable today and covered by its published qualification, but source, package, target-profile and ABI contracts may still change between minor releases. The 1.0 roadmap above defines the compatibility commitment still to be earned.
Development hosts: macOS ARM64, Linux ARM64/x64 and Windows ARM64/x64. The normal portable output is a wasm32 WASI Preview 2 Component. Raw wasm32/wasm64 and JavaScript-host integration are advanced alternatives; wasm64 Components remain blocked by upstream tooling.
Runtime reflection, dynamic, runtime assembly loading and managed threading
are outside the profile. General System.IO.File/Directory/FileStream,
raw sockets and subprocess APIs are not implemented. Existing NuGet packages
work only when their reachable code and API dependencies fit the supported
profile, not merely because they target .NET.
Read the support inventory before porting an app. The documentation index covers hosting, diagnostics, runtime contracts and deployment.
License
CoreLib, runtime libraries, templates and generated support code are MIT, subject to preserved upstream notices. The compiler and developer tooling use the NetWasm Community License 1.0, not MIT. The Compact collector retains its Apache-2.0 license and AssemblyScript attribution; see third-party notices.
The Community License is free for qualifying individuals, education, open-source work, evaluation, and organizations with fewer than 250 employees and less than USD 10,000,000 in annual revenue. Matching GitHub Sponsors tiers grant limited internal commercial use; OEM and Commercial Offering use require a separate agreement. Generated applications may use their authors' chosen licenses. There is no sign-in, activation, telemetry or technical enforcement.
See the licensing guide for the exact boundaries. Commercial licensing: zionsatidev@gmail.com.
Bring a workload
Start with Console.WriteLine(42), then try the smallest real workload where
deployment size, sandboxing, or portability matters. If something is missing,
open an issue, concrete workloads
are how the 1.0 support boundary gets decided.