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?







