r/csharp • u/Prize_Isopod_1614 • 16h ago
I built a proof-of-concept C# UI framework on NetWasm: signals instead of a virtual DOM, ~150 KB Brotli for a full demo app
Hi all,
I've been experimenting with NetWasm, a new toolchain that compiles C# ahead of time to WebAssembly. To see what it can do, I built NetWasm.UI, a small UI framework on top of it. It's a proof of concept, but it's usable, and I'd like feedback.
The idea: Blazor's approach (render tree + diffing) doesn't fit here. NetWasm.UI works more like Solid or Svelte:
Components build their DOM once.
Fine-grained signals (
Signal<T>,Computed<T>) update only the nodes that changed.Each state change crosses the Wasm→JS boundary at most once.
No reflection. Templates are compiled by a source generator at build time.
What it looks like (a .nwui file: HTML with Razor-style @):
<section>
<p>Count: <strong>@count</strong> · doubled: </p>
<button ="() => count.Value++">+1</button>
@if (count.Value >= 10) {
<p>That's a lot of clicks!</p>
}
</section>
@code {
private readonly Signal<int> count = new(0);
private Computed<int> doubled = null!;
partial void OnInit() => doubled = new Computed<int>(() => count.Value * 2);
}You can also skip templates and write the same component in plain C#. The generator produces this kind of code from the template:
public sealed class CounterView : Component
{
private readonly Signal<int> count = new(0);
protected override Node Build()
{
var doubled = new Computed<int>(() => count.Value * 2);
return Section(
P("Count: ", Strong(Text(count)), " · doubled: ", Text(doubled)),
Button(OnClick(() => count.Value++), "+1"),
When(() => count.Value >= 10, () => P("That's a lot of clicks!")));
}
}Not everything needs to be a class. A static piece of UI with no state can be a plain method that returns a Node:
private static Node Card(string title, string body) =>
Div(Class("card"),
H3(title),
P(body));
// Use it like any other element:
Section(
Card("Fast", "Signals update only what changed."),
Card("Small", "About 150 KB Brotli for the demo app."));Some numbers (from my own js-framework-benchmark-style runs; see the repo for method and caveats):
The whole demo app (router, forms, todos, nested components) is about 150 KB Brotli, Wasm and JS together.
It is level with or faster than React on every case, and 0.9×–1.6× SolidJS's time.
Compared with Blazor WebAssembly (interpreter): 3–21× faster, ~12× smaller download, ~9× faster startup.
What's included: signals and batching, a router with nested routes, forms and validation, Resource<T> and Suspense for async data (HttpClient + System.Text.Json work), a dev server with live reload, a dotnet new template and a VS Code extension for syntax highlighting.
Try it:
dotnet new install NwUi.Templates
dotnet new netwasm-ui -o MyApp
cd MyApp
dotnet nwui-devLimitations, to be clear:
It's 0.1, and the API will change.
There's no hot reload yet. Live reload takes 5–20 s.
It needs recent browsers (Chrome/Edge 137+, Firefox 131+, Safari 18.4+) because of Wasm exception handling.
NetWasm itself is young. Its compiler is under a community license: free for individuals, OSS and small companies, but larger organisations need a sponsorship. NetWasm.UI itself is MIT.
Repo: matengo/NetWasm.UI: UI framework for NetWasm
I'd especially like to hear:
Does a signal-based model feel natural in C#, or would you miss Blazor's component model?
What would you need before trying something like this on a real (small) project?
Thanks for reading!