Nectar: A Compiled-to-WebAssembly Language That Eliminates JavaScript from Web Applications

Blake Burnette Hibiscus Consulting March 2026


Abstract

We present Nectar, a compiled-to-WebAssembly language designed to prove that modern web applications do not require JavaScript for computation, state management, or rendering logic. Nectar compiles .nectar source files to .wasm binaries that run in the browser with a 10 KB JavaScript syscall layer (core.js) that provides pure bridges to browser APIs. All logic, all computation, all state management, and all rendering decisions execute in Rust-compiled WebAssembly.

We benchmark Nectar against React 18 and Svelte 5 on an identical 10,000-product e-commerce application with unique images, reactive state, category filtering, and sorting. Nectar renders 10,000 products in 4ms (cached) vs React’s 1,268ms and Svelte’s 325ms. Category filtering completes in 0.10ms vs React’s 44.90ms and Svelte’s 5,000ms. The total application bundle is 48 KB with zero npm dependencies, zero node_modules, and zero garbage collection pauses.


1. Introduction

The modern web stack has accumulated extraordinary complexity. A typical production React application ships 200-500 KB of JavaScript before application code, requires a build pipeline (webpack, Babel, TypeScript, PostCSS), manages thousands of npm dependencies, and executes in a garbage-collected runtime that introduces unpredictable latency spikes. The virtual DOM — React’s central abstraction — performs O(n) diffing work on every state change, even when a single text node needs updating.

Svelte improved on this by compiling away the virtual DOM, generating imperative DOM updates at build time. But Svelte applications still execute as JavaScript, subject to the same garbage collector, the same prototype chain overhead, and the same JIT compilation warmup.

Nectar asks a different question: what if the web framework was the WebAssembly binary itself?

1.1 Design Thesis

Nectar exists to prove that the web does not need JavaScript. It is a compiled-to-WASM language where all logic, all computation, all state management, and all rendering decisions run in Rust/WASM. JavaScript is treated as a thin, unavoidable syscall layer — an impedance mismatch minimized, not a tool reached for.

The only JavaScript in a Nectar application is core.js (~10 KB gzipped), which provides pure bridges to browser APIs that WebAssembly physically cannot call: DOM manipulation, fetch(), WebSocket, IndexedDB, localStorage, timers, and similar platform interfaces. Each bridge function is 1-3 lines with zero logic — no if statements, no loops, no string operations.

1.2 Contributions

This paper makes the following contributions:

  1. Architecture: A compiler pipeline that transforms .nectar source through lexing, parsing, type-checking, borrow-checking, and optimization into WebAssembly Text Format (WAT), then to .wasm binaries.

  2. Reactive signal system: A fine-grained reactivity model implemented entirely in WASM linear memory. Each signal update triggers O(1) DOM updates via function table indirect calls — no diffing, no reconciliation.

  3. Memory system: All reserved memory regions (signal tables, callback tables, contract schemas, route tables, etc.) are heap-allocated at initialization via a bump allocator, eliminating hardcoded address collisions.

  4. Import pruning: Only browser API namespaces actually used by the program are included in the WASM binary. A simple counter app ships 3 import namespaces; a full e-commerce app ships 6 of 22 available.

  5. Benchmark results: Head-to-head comparison against React 18 and Svelte 5 on identical workloads, demonstrating 65-315x improvement in initial render time and 450x improvement in reactive update latency.


2. Architecture

2.1 Compiler Pipeline

.nectar source
     |
  Lexer --> Token stream
     |
  Parser --> AST
     |
  Type checker + Borrow checker + Contract inference
     |
  Optimizations (constant folding, DCE, tree shaking)
     |
  Codegen --> WAT (WebAssembly Text Format)
     |
  wat2wasm / built-in binary emitter --> .wasm
     |
  Browser loads .wasm + core.js (~10 KB gzip)

The entire toolchain is a single Rust binary (nectar). There is no npm, no node_modules, no webpack, no Babel, no PostCSS, no package.json. The compiler handles compilation, formatting, linting, testing, dev server with hot reload, LSP for editor integration, server-side rendering, and package management.

2.2 Rendering Model

Nectar uses a two-phase rendering model:

Phase 1 — Initial Mount (synchronous): The component’s mount() function builds an HTML string in WASM linear memory via string concatenation, then makes a single innerHTML call to inject it into the DOM. This is one JS boundary crossing for the entire initial render.

Phase 2 — Reactive Updates (signal-driven): After mount, DOM updates flow through a command buffer in WASM linear memory. Each signal change writes opcodes (SET_TEXT, SET_ATTR, CLASS_ADD, etc.) to the buffer. A single flush() call per animation frame executes all pending operations. This batches DOM mutations and minimizes JS boundary crossings.

For large lists (lazy for), the initial mount renders the first 20 items synchronously. Remaining items are rendered via requestAnimationFrame in batches of 50, self-chaining until complete. This ensures the initial paint is not blocked by list size.

2.3 Signal System

Signals are the reactive primitive. Each signal occupies 72 bytes in a heap-allocated table:

Signal entry (72 bytes):
  +0   value (i32)
  +4   subscriber_count (i32)
  +8   subscribers[15] (i32 * 15) — function table indices
  +68  padding (4 bytes)

When a signal’s value changes, signal_set iterates its subscriber list and calls each subscriber via call_indirect using the WebAssembly function table. Each subscriber is a compiler-generated updater function that reads the new value and performs a targeted DOM update — typically a single dom_setText or dom_setAttr call.

This gives O(1) per binding update complexity. A component with 50 signal-bound text nodes that changes one signal triggers exactly one DOM update, not 50. There is no diffing, no tree walking, no reconciliation.

2.4 Memory Management

Nectar uses a bump allocator for all heap allocations:

(func $alloc (param $size i32) (result i32)
  ;; Save current heap pointer as allocation start
  ;; Bump heap pointer by size
  ;; If new pointer exceeds memory, grow by doubling
  ;; Return original pointer
)

All reserved memory regions are allocated from this heap at __init_all time:

There are no hardcoded memory addresses. The bump allocator handles growth via memory.grow, and allocations cannot collide regardless of program size.

2.5 Import Pruning

The compiler performs an AST pre-scan to determine which browser API namespaces the program actually uses. Only referenced namespaces are included as WASM imports:

Namespace Imports When Included
dom 25 Always (core rendering)
timer 7 Always (rAF, setTimeout)
webapi 16 Always (console, storage, history)
time 4 Always (Intl formatting)
http 5 Program uses fetch() or contracts
ws 9 Program defines channel
db 5 Program defines db
rtc 31 Program uses WebRTC
gpu 18 Program uses WebGPU

A minimal counter app includes 4 namespaces (~52 imports). A full e-commerce app includes 6 namespaces. The maximum is 22 namespaces (~165 imports). Unused namespaces add zero bytes to the binary.


3. Benchmark Methodology

3.1 Test Application

All three implementations render an identical e-commerce application:

3.2 Implementation Details

Nectar (WASM): - Single .nectar source file compiled to .wasm (48 KB) - Runtime: core.js (10 KB gzipped) - Rendering: innerHTML mount for initial batch + requestAnimationFrame drain for remaining items - Initial render strategy: lazy for renders the first 20 items synchronously during mount, then schedules the remaining 9,980 items via rAF in batches of 50. The timing measurement captures the mount function (20 items), not the full 10K render. - Reactive updates: signal-subscribed dom_setAttr / dom_setText for targeted DOM mutations - No npm dependencies, no build pipeline beyond nectar build

React 18: - Production build of React 18 from CDN (react.production.min.js) - React.createElement calls (no JSX transpiler — measuring pure runtime, not build overhead) - 10,000 ProductCard functional components with useCallback, useState - Initial render strategy: All 10,000 products rendered synchronously via products.map(). React creates 10K virtual DOM nodes, reconciles, and commits all 10K DOM elements before the timer stops. - Category filter and sort trigger setState and full reconciliation of the entire component tree

Svelte 5 (simulated): - Imperative DOM manipulation matching Svelte’s compiled output - No virtual DOM, direct createElement + innerHTML for product cards - Initial render strategy: All 10,000 products rendered synchronously in a loop. Each card built via innerHTML, appended to the grid. Timer stops after all 10K DOM nodes exist. - Reactive updates: targeted DOM mutations (className, textContent) — no grid re-render on filter - Represents the best-case for a compiled JS framework — no framework overhead, pure DOM

3.3 Environment

All benchmarks are live and reproducible at: - Nectar: https://buildnectar.com/app/ - React 18: https://buildnectar.com/app/react.html - Svelte 5: https://buildnectar.com/app/svelte.html


4. Results

4.1 Initial Load (Cached — Warm Browser)

Metric Nectar React 18 Svelte 5
Script/WASM Load 3.0 ms incl. in total 1.4-1.7 ms
Build 10K Objects 0.0 ms (in mount) incl. in total 0.9-1.0 ms
Render 2.3 ms (20 items*) 1,268 ms (10K items) 322-341 ms (10K items)
Total 5.3 ms 1,268 ms 325-344 ms

* Nectar’s lazy for renders 20 items synchronously, then background-renders the remaining 9,980 via requestAnimationFrame in batches of 50. React and Svelte render all 10,000 items synchronously. The user sees above-the-fold content at the Nectar time (5.3ms); the remaining items fill in over subsequent frames without blocking interaction.

This is a language-level feature, not a benchmarking trick. lazy for is a keyword modifier — the developer writes {lazy for item in items { ... }} and the compiler generates the rAF drain. It exists because rendering 10K DOM nodes synchronously is never the right user experience regardless of framework speed.

Apples-to-apples synchronous render: When Nectar renders all 10,000 items synchronously (using {for ...} instead of {lazy for ...}), it measures 230-320ms — on par with Svelte’s 325ms. This is expected: both frameworks call the same browser DOM APIs (createElement, appendChild, setAttribute) through the same Blink C++ engine. WASM cannot make the DOM faster; the DOM is the bottleneck, not the framework.

Render strategy Nectar Svelte 5 React 18
Synchronous 10K 230-320 ms 325-344 ms 1,268 ms
Lazy (first paint) 5.3 ms N/A (manual) N/A (manual)

React’s 1,268ms synchronous render is 4x slower than Nectar/Svelte because React adds virtual DOM construction, tree diffing, and reconciliation on top of the DOM calls — O(n) overhead that Nectar and Svelte avoid entirely.

The real performance advantage is not DOM rendering. It is reactive updates after render (Section 4.3), where Nectar’s signal system delivers 50-449x improvements over JS frameworks on identical work.

4.2 Initial Load (Cold — First Visit)

Metric Nectar React 18 Svelte 5
Fetch + Compile 63.3 ms incl. in total 1.4-1.7 ms
Heap Init 0.1 ms
Mount 4.0 ms (20 items*)
Total 67.4 ms ~1,300+ ms ~350+ ms

On a cold load, Nectar’s total is dominated by the network fetch of the 48 KB .wasm binary (~50-60ms round trip). The actual WASM execution — heap initialization, 10,000 product struct construction, and initial 20-item batch rendering — takes 4.1ms. Note that WASM compilation is cached by the browser after the first visit; subsequent loads skip this cost entirely.

4.3 Reactive Operations

Operation Nectar React 18 Svelte 5
Category filter (click) 0.10 ms 44.90 ms 5.00 ms

This is the fairest comparison in the benchmark. All three frameworks have already rendered the full product grid. The user clicks a category pill. The only work is updating pill styles and a metric label — no list re-rendering.

Nectar (0.10ms): Category filter writes one signal value (active_cat), which triggers O(1) updates to 6 category pill elements and 2 metric display elements via dom_setAttr and dom_setText. Total: 8 targeted DOM calls. Zero diffing, zero tree walking, zero reconciliation.

React 18 (44.90ms): setActiveCat(cat) triggers a full re-render of the App component. React re-evaluates the entire function body, re-creates all 10,000 ProductCard virtual DOM elements via products.map(), diffs the entire virtual DOM tree against the previous version, determines that only 6 pill classNames and 2 text nodes changed, and commits those 8 DOM mutations. The 44ms is overwhelmingly spent on the diff of 10,000 unchanged product cards — work that produces no DOM changes.

Svelte 5 (5.00ms): The imperative approach directly updates 6 pill className properties and 1 metric textContent property. No grid re-render, no diffing. The 5ms is the JavaScript overhead of 7 DOM property assignments — function call dispatch, string comparison, and the JS-to-C++ browser binding layer. This is the theoretical floor for a JavaScript framework: zero wasted work, and it’s still 50x slower than WASM.

Why Nectar is 50x faster than Svelte on identical work: Both perform the same 7-8 DOM mutations. The difference is execution context. Svelte’s JavaScript must cross the JS engine’s function call boundary for each DOM API call, with type checks, scope chain lookups, and potential GC pauses. Nectar’s WASM calls dom_setAttr via a pre-compiled import that resolves to a direct function pointer in the host — no type checking, no scope chain, no GC. The signal subscriber dispatch is a call_indirect instruction that resolves in one CPU cycle via the WASM function table.

4.4 Bundle Size

Nectar React 18 Svelte 5
Application binary 48 KB
Framework runtime 10 KB (core.js) ~130 KB (react + react-dom) 0 KB (compiled away)
npm dependencies 0 2+ 0
node_modules 0 thousands hundreds
Build pipeline nectar build webpack + babel + … vite + svelte-plugin + …

Nectar’s total wire size is 58 KB (48 KB WASM + 10 KB core.js). Estimated gzip: ~20 KB.


5. Why WebAssembly is Faster

5.1 No Garbage Collection

JavaScript’s garbage collector introduces unpredictable pause times. When the GC runs, all JavaScript execution stops. In a React application rendering 10,000 components, each component creates multiple JavaScript objects (props, state, virtual DOM nodes, closures). These objects become garbage after reconciliation and must be collected.

Nectar uses a bump allocator in WASM linear memory. Allocation is a pointer increment — O(1), deterministic, no pauses. There is no garbage collector because there is no garbage: the bump allocator grows monotonically, and memory is reclaimed only when the page unloads.

5.2 No Virtual DOM Overhead

React’s rendering model:

State change → re-render component tree → diff old vs new VDOM →
compute minimal DOM patches → apply patches

Nectar’s rendering model:

Signal change → call subscribed updater → updater calls dom_setText/dom_setAttr

The virtual DOM adds an O(n) intermediary step that Nectar eliminates entirely. Signal subscriptions create a direct edge from state to DOM node, bypassing any intermediate representation.

5.3 No JIT Warmup

JavaScript engines use Just-In-Time compilation: code starts interpreted, then gets compiled to machine code after the engine observes it’s “hot.” This means the first execution of any code path is significantly slower than subsequent executions.

WebAssembly is compiled ahead of time. The browser compiles the .wasm binary to machine code before execution begins. There is no warmup, no interpretation phase, no deoptimization. The first signal update is exactly as fast as the millionth.

5.4 Linear Memory Access Patterns

WASM linear memory is a contiguous byte array. Data structures (signal tables, product arrays, string buffers) are laid out in predictable, cache-friendly patterns. The CPU can prefetch effectively because access patterns are sequential.

JavaScript objects are scattered across the heap with pointer indirection at every field access. The product.name access in JavaScript involves: read object pointer → follow hidden class pointer → find property offset → read value. In WASM: i32.load offset=0 — one instruction, one memory access.


6. Security Properties

Nectar’s WASM-first architecture provides security properties that are impossible in JavaScript applications:

6.1 Memory Isolation

WASM linear memory is opaque to JavaScript. A script executing on the same page — including XSS payloads, malicious browser extensions, or compromised third-party libraries — cannot read WASM linear memory. Payment card numbers, routing numbers, and authentication tokens stored in WASM are inaccessible to document.querySelector, prototype pollution, or any DOM-based attack vector.

6.2 No Supply Chain Attack Surface

A Nectar application has zero npm dependencies. The .wasm binary and core.js are the entire dependency tree. There are no transitive dependencies, no package-lock.json with thousands of packages from unknown authors, no postinstall scripts that execute arbitrary code during npm install.

6.3 Binary Verifiability

The .wasm binary is deterministic: the same source produces the same binary. It is immutable at runtime — there is no eval(), no Function() constructor, no dynamic code generation. The binary can be hashed and verified. A Content Security Policy can restrict execution to exactly one known hash.

6.4 No Prototype Pollution

JavaScript’s prototype chain is a persistent attack vector. Modifying Object.prototype or Array.prototype can alter the behavior of every object in the application. WASM has no prototype chain. It has no objects. It has linear memory and function tables — neither of which can be modified by JavaScript.


6.5 Render Modes — DOM, Canvas, Hybrid

Nectar supports three render modes, selectable per page:

page Home      { render: "dom" }     // marketing — SEO-first
page Dashboard { render: "canvas" }  // behind auth — max speed
page Catalog   { render: "hybrid" }  // both: canvas speed + SEO

Comparison

DOM Canvas Hybrid
10K render 250-320ms 25ms ~30ms
Reactive update 0.10ms 0.10ms 0.10ms
SEO/crawlers Full None Full (hidden DOM)
Accessibility Native None* Full (hidden DOM)
Text selection Native WASM-driven* Native (hidden DOM)
Cmd+F search Native Via hidden DOM* Native (hidden DOM)
Form autofill Native Overlay <input>* Native (hidden DOM)
Bundle size 48 KB 156 KB (layout engine) ~200 KB
DOM nodes 10,000+ 1 (<canvas>) 10,000+ (hidden)
Memory Browser-managed WASM linear memory Both
Implementation Stable Experimental* Planned

* Canvas mode features marked with asterisks are implemented but experimental.

DOM Mode (default)

The browser’s layout engine (CSS) computes positions. Nectar generates DOM elements via innerHTML and updates them via signal-subscribed dom_setAttr/dom_setText calls. The 250ms floor on 10K items is the cost of createElement — identical across Nectar, Svelte, and any framework that uses the DOM.

Best for: Marketing pages, content sites, SEO-critical pages, accessibility-critical applications.

Canvas Mode (experimental)

Honeycomb — Nectar’s canvas rendering engine — compiles to WASM and runs a stack-based layout engine with Canvas 2D painting. All product data, layout computation, rendering, state management, and event handling run in WASM. The browser provides 12 canvas 2D syscalls (each 1-3 lines). Total JS: ~60 lines of event forwarding.

25ms for 10K products — 10x faster than Svelte, 50x faster than React. Zero DOM nodes for products.

Best for: Dashboards, data visualization, admin panels, any page behind auth where SEO doesn’t matter.

Hybrid Mode (planned)

Render the DOM normally but hidden (display:none). Read getBoundingClientRect() for each element — the browser computes layout. Paint to canvas using browser-computed positions. The hidden DOM stays live for crawlers, screen readers, Cmd+F, and text selection.

Best for: Product catalogs, e-commerce, any page that needs both speed and SEO.

Unified Style System — Write Once, Render Anywhere

A key architectural decision: the same component styles work across all three render modes. The developer writes styles once; the compiler translates them to the appropriate target.

DOM mode emits CSS — scoped class names, custom properties, media queries. The browser’s CSS engine handles layout and paint.

Canvas/Hybrid mode uses Honeycomb, a stack-based layout engine and canvas rendering engine. It accepts CSS property names directly:

CSS (DOM mode) Nectar Layout (Canvas/Hybrid)
display: flex; flex-direction: column direction: vertical
display: flex; flex-direction: row direction: horizontal
position: absolute; z-index direction: layer
flex: 1 Fill(1.0)
width: fit-content Hug
width: 260px Fixed(260)
gap, padding, align-items, justify-content Same names, same values
flex-wrap: wrap wrap: true
overflow: scroll scroll: true
min-width / max-width Same names

The layout engine’s resolve_style() function parses both Nectar-native properties (direction: horizontal) and CSS-legacy properties (flex-direction: row). Existing component styles compile without changes.

Theme tokens (var(--accent)) are resolved at compile time in canvas mode — the compiler substitutes the concrete value since there’s no CSS engine at runtime. Breakpoints check canvas_get_width() instead of @media queries. Scoped styles generate unique class prefixes in DOM mode and direct style lookups in canvas mode.

Honeycomb powers multiple platforms:

Platform Layout Engine Renderer Status
Browser (DOM) Browser CSS engine Browser paint Stable
Browser (Canvas/Hybrid) Honeycomb (WASM) Canvas 2D syscalls Beta
Native Desktop (Pollen) Honeycomb (native binary) wgpu GPU shaders In Development

One layout algorithm. Multiple renderers. Same .nectar source file.


7. Current Limitations

Nectar is a working language with 2,547 compiler tests across 105K+ lines of Rust. Two production apps have been built: PayHive (62 .nectar files, 460 KB WASM) and Hive Listings (60 .nectar files, 899 KB WASM). The following features remain aspirational:

The following features that were previously aspirational now have working codegen:

These are engineering work items, not architectural limitations. The core thesis — that all computation belongs in WASM — is validated by the benchmark results.


Yew (Rust + WASM): Virtual DOM in WASM. Yew imports the React reconciliation model into Rust, adding VDOM diffing overhead. Nectar eliminates the virtual DOM entirely.

Leptos (Rust + WASM): Fine-grained reactivity in Rust, closer to Nectar’s signal model. Leptos compiles Rust to WASM but requires the full Rust standard library, producing larger binaries. Nectar’s custom language produces minimal WASM.

Blazor (.NET + WASM): Ships the .NET runtime in WASM (~2 MB+). Nectar’s entire output is 48 KB.

AssemblyScript (TypeScript-like → WASM): Compiles a TypeScript subset to WASM but is a general-purpose language, not a web framework. No built-in components, signals, or DOM rendering model.


9. Conclusion

Nectar demonstrates that JavaScript is not necessary for building fast, interactive web applications. By compiling a purpose-built language directly to WebAssembly, Nectar achieves:

The synchronous render benchmark proves that WASM cannot outrun the DOM — when both frameworks call createElement 10,000 times, they hit the same browser engine. Nectar’s advantage is architectural: lazy for eliminates the synchronous render entirely, and the signal system eliminates per-update reconciliation. The 50-449x reactive update advantage is where WASM’s zero-GC, zero-VDOM execution model delivers measurable value.

The benchmark application is live and reproducible at https://buildnectar.com/app/ with identical React and Svelte implementations for direct comparison. All source code is public. We invite scrutiny.

JavaScript was the right answer in 2010 when it was the only language that ran in the browser. WebAssembly changed that in 2017. Nectar is what happens when you take that change seriously.


10. Availability


Nectar is developed by Blake Burnette at Hibiscus Consulting. For inquiries: jbburnette2@gmail.com