Table Of Contents
- The Bottleneck
- Step 1: The Memory Slab
- Step 2: JSExport Bridge
- Step 3: DOM Mutation
- The Performance Payoff
- Compile-time Generation with Roslyn
- Try it yourself!
The Bottleneck: When WebAssembly UI gets sluggish
If you've ever built a heavy data grid, a real-time telemetry dashboard, or a line-of-business spreadsheet application in Blazor WebAssembly, you've likely hit the rendering wall.
Blazor WASM is incredibly productive, but when thousands of cells change multiple times a second:
- Every property change triggers an event.
- Every event triggers standard Blazor component cycle overhead (
StateHasChanged). - The Virtual DOM (VDOM) diffs the old rendering tree with the new one.
- JS-Interop serializes the diffs and updates the browser DOM.
Under high frequency (like 100+ updates per second on 5,000+ fields), this pipeline stutters. The CPU spikes to 100%, garbage collection (GC) triggers pauses, and key-press latency rises.
We decided to see if we could completely bypass Blazor's rendering pipeline while maintaining C# as the single source of truth for business logic.
Our solution is V.A.L.I.D.—a compile-time state-tracking framework that shares raw WebAssembly memory slabs directly with JavaScript.
Step 1: The Unmanaged Memory Slab in C
To avoid heap allocations and GC overhead when managing object states (dirty, error, busy, deleted), we allocate a contiguous block of native memory on the WebAssembly linear heap.
We built a custom bump allocator wrapper called UnmanagedSlab<T> that uses NativeMemory.Alloc:
public unsafe class UnmanagedSlab<T> : IDisposable where T : unmanaged
{
private T* _pointer;
private readonly int _length;
public UnmanagedSlab(int length)
{
_length = length;
// Allocate contiguous memory in the WASM heap
_pointer = (T*)NativeMemory.Alloc((nuint)length, (nuint)sizeof(T));
}
public ref T this[int index] => ref _pointer[index];
public void* GetUnsafePointer() => _pointer;
// Dispose calls NativeMemory.Free...
}
Every business object in the application is registered to a fixed slot in this slab. Its state flags are represented as single bits in a System.UInt128 mask register, giving us up to 128 fields tracked per object in O(1) time with 0 heap allocations.
Step 2: The JSExport Bridge
Next, we need to let JavaScript know where this memory block is. We use .NET 8's new JS interop system with [JSExport] inside our WebWorkerBridge class:
public static partial class WebWorkerBridge
{
private static UnmanagedSlab<System.UInt128>? _stateSlab;
[JSExport]
public static unsafe nint GetStatePointer()
{
if (_stateSlab == null) return 0;
return (nint)_stateSlab.GetUnsafePointer();
}
}
When our Blazor page initializes, we retrieve this pointer and pass it directly to JS as a long integer address:
var ptr = WebWorkerBridge.GetStatePointer();
var len = WebWorkerBridge.GetSlabLength();
await JSRuntime.InvokeVoidAsync("__VAVID_INITIALIZE_INDUSTRIAL_BYPASS__", (long)ptr, len);
Step 3: Surgical DOM Mutation in JS
Once JavaScript receives the memory address, it connects to Mono's WebAssembly linear heap buffer (HEAPU8 array).
First, we compile a surgical map of DOM inputs. Every VavidInput.razor component renders standard metadata data-attributes rather than binding inputs to Blazor:
<input class="vavid-control"
value="@Value"
data-vavid-slab-index="@SlabIndex"
data-vavid-bit="@BitIndex" />
Our JS code parses these elements on startup and runs a high-performance requestAnimationFrame loop.
Because we know the exact byte offsets, JS can read the dirty/error/busy bitmasks directly from the heap array and surgically toggle class names without going through the VDOM:
function syncSurgicalMap() {
const h = globalThis.Module ? globalThis.Module.HEAPU8 : null;
const base = statePointer; // the nint address from C#
for (let i = 0; i < surgicalMap.length; i++) {
const entry = surgicalMap[i];
const ptr = base + entry.offset; // slot offset
// Read dirty and error bit flags directly from WASM memory
const isDirty = (h[ptr + entry.bitByte] & entry.bitMask) !== 0;
const hasError = (h[ptr + 32 + entry.bitByte] & entry.bitMask) !== 0;
// Toggle UI classes instantly in place
const el = entry.el;
if (isDirty) el.classList.add('vavid-dirty'); else el.classList.remove('vavid-dirty');
if (hasError) el.classList.add('vavid-error'); else el.classList.remove('vavid-error');
}
}
The Performance Payoff
We ran micro-benchmarks comparing this bypass model to standard Blazor VDOM mutation and F# rules engines:
| Method | Mean | Gen 0 / 1000 | Allocated | Speedup |
|---|---|---|---|---|
| VALID Slab direct memory write | 6.62 ns | - | 0 B | 26.7x |
| F# Rule Evaluation | 15.80 ns | - | 0 B | 10.4x |
| Blazor VDOM Mutation (Baseline) | 172.78 ns | 0.0048 | 40 B | Baseline |
At 6.6ns per update with 0 bytes allocated, state tracking is running at register-level CPU efficiency.
By avoiding Blazor's entire component render cycle, UI grids can handle millions of visual updates with zero input latency.
Compile-time Generation with Roslyn
Writing manual pointer offsets and bit masks by hand for every class is exhausting. To keep the developer experience clean, we built a Roslyn Source Generator.
All you do is write a partial class:
[ValidObject]
public partial class Invoice
{
[Required]
public string CustomerName { get; set; } = "";
[Range(0, 10000)]
public decimal Amount { get; set; }
}
The source generator intercepts this at build time and generates the bitmask backing properties, circular undo/redo history, F# record projections, and auto-generates unit/fuzz tests right into your test projects.
Try it yourself!
V.A.L.I.D. is fully open-source. If you want to check out the WASM memory management, review the source generators, or run the Blazor grid benchmark locally, check out the repository:
UnitBuilds-CC
/
V.A.L.I.D.
V.A.L.I.D. (Vectorized Asynchronous Logic & Intelligent Diagnostics)
V.A.L.I.D. (Vectorized Asynchronous Logic & Intelligent Diagnostics)
V.A.L.I.D. is a high-performance, low-latency business logic framework for .NET. It replaces standard reflection-based change tracking with a compile-time, bitmask-driven, and compiler-integrated architecture.
Built specifically for complex enterprise data management, V.A.L.I.D. ensures surgical precision in synchronization and real-time visibility in the browser.
⚡ Performance Benchmarks
V.A.L.I.D. is engineered for absolute zero-allocation on core operations. Below are the official BenchmarkDotNet results comparing V.A.L.I.D.'s direct memory write speed against other state-management components:
BenchmarkDotNet v0.13.12, Windows 11 (10.0.29591.1000)
.NET SDK 10.0.200-preview.0.26103.119
[Host] : .NET 8.0.24 (8.0.2426.7010), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.24 (8.0.2426.7010), X64 RyuJIT AVX2
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|---|---|---|---|---|---|
| VALID Slab direct memory write | 6.619 ns | 0.1573 ns | 0.2305 ns | - | - (0 B) |
| F# Rule Evaluation | 15.800 ns | 0.3659 ns | 0.7308 ns | - | - (0 B) |
| F# CRDT Convergence | 86.478 ns | 1.7367 ns | 3.7384 ns | 0.0391 | 328 B |
| Blazor VDOM Mutation |
I’d love to hear your thoughts on this. Is sharing raw pointers with JS in WebAssembly too unsafe for typical business applications, or is the performance payoff worth the trade-off?
Top comments (1)
Agentic browser MCP (coming soon) utilizing Neo4J graphDb for site-mapping AOM, to allows for shortest path finding for workflow automation and automated testing. Love to hear the community's thoughts on it, so far it's an average of 8x faster and more token efficient than standard browser agents (Puppeteer + DOM scraping + Screenshots).