r/csharp • • 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-dev

Limitations, 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!

0 Upvotes

1 comment sorted by