Skip to content

Architecture: Rust & WebAssembly

Nodus is not a JavaScript library with a few fast paths bolted on. Its compute and rendering core is written in Rust and compiled to WebAssembly (WASM). The JavaScript/TypeScript layer you import is a thin facade: it preserves an ergonomic, typed API (Nodus<NodeData, EdgeData>) and owns the DOM, while the heavy lifting happens in compiled code.

This page explains how the pieces fit together. For how pixels actually reach the screen, see The Rendering Pipeline.

The big picture

┌─────────────────────────────────────────────────────────────┐
│ @kortexya/nodus — TypeScript facade │
│ • preserves `import { Nodus }` and `Nodus<ND, ED>` generics │
│ • owns the container, holds your JS node/edge `data` │
│ • routes calls across the WebAssembly boundary │
└───────────────┬─────────────────────────────────────────────┘
│ wasm-bindgen boundary
┌───────────────▼─────────────────────────────────────────────┐
│ nodus-wasm — the boundary crate │
│ • JS ↔ WASM API surface, render loop, DOM/event capture │
└───────┬───────────────────────────────────┬─────────────────┘
│ │
┌───────▼────────────────────┐ ┌───────────▼─────────────────┐
│ nodus-core │ │ nodus-render │
│ pure compute (no DOM): │ │ GPU renderer on wgpu: │
│ graph model, topology, │ │ scene batching, attribute │
│ spatial index, geometry, │ │ packing, camera, LOD, │
│ transforms, layouts, │ │ picking, WGSL shaders │
│ hypergraph stack │ │ (loaded as nodus-render- │
│ │ │ wasm, a second WASM module)│
└────────────────────────────┘ └─────────────────────────────┘

The crates

Nodus is a Rust Cargo workspace of four crates that compile to two WASM modules (one for compute, one for rendering) plus, where relevant, native libraries.

nodus-core — pure compute

The heart of the engine. It has no web dependencies, so it compiles to both WebAssembly and native code. It contains:

  • the graph model, stored as struct-of-arrays in linear memory;
  • topology queries (adjacency, degree, connected components);
  • a spatial index (quadtree + BVH) for hit-testing and neighbour queries;
  • geometry (distances, intersections, polygons, Bézier curves);
  • transformations (grouping, meta-edges, edge bundling);
  • every layout — force, force-link, hierarchical (Sugiyama), grid, radial, concentric and sequential; and
  • the hypergraph stack (simplification, optimization, statistics).

nodus-render — the GPU renderer

A renderer built on wgpu, the Rust GPU abstraction. It owns scene batching, attribute packing, the camera, level-of-detail and picking, and its WGSL shaders for nodes, edges and text. On the web it targets WebGPU; the same code targets native Vulkan/Metal/DX12 elsewhere.

nodus-wasm — the boundary

The wasm-bindgen crate that exposes the API surface to JavaScript. It owns the requestAnimationFrame loop, the DOM container, and pointer/keyboard event capture (through web-sys).

nodus-render-wasm — the renderer module

The GPU renderer is compiled to its own WebAssembly module and loaded lazily, only when GPU rendering is actually used. Keeping it separate keeps the initial load small for graphs that render on the Canvas 2D path.

The TypeScript facade

The published package is a small, fully-typed TypeScript layer. Its jobs are to:

  • preserve the public APINodus, Node, Edge, NodeList, EdgeList, the geometry / parse / hypergraph namespaces, and the layout factories — with hand-written generic types so Nodus<ND, ED> stays precise;
  • own the DOM — the container, canvas, and event listeners;
  • hold your data — the arbitrary JavaScript objects you attach as node/edge data live JS-side (see below); and
  • route calls across the WebAssembly boundary.

How data crosses the boundary

Nodus deliberately splits where data lives, rather than copying everything back and forth each frame:

  • In Rust (WASM linear memory): the genuinely numeric, typed columns — positions, radii, edge endpoints, flags, numeric style attributes. These are packed contiguously for cache-friendly, SIMD-friendly compute.
  • In JavaScript: the arbitrary values you attach — each node’s and edge’s data (your ND/ED objects) — plus string dictionaries. These are indexed by the same dense index Rust uses, so a node is one identity on both sides with no serialization.

The result: layouts and geometry run over tight typed arrays in compiled code, while your application objects stay as ordinary JavaScript you can read and mutate directly (node.getData()).

Threading

By default, expensive operations — running a layout, optimizing a hypergraph — execute in a Web Worker, with the WASM module instantiated inside the worker so the main thread stays responsive. This uses ordinary postMessage and therefore needs no cross-origin isolation headers, which keeps Nodus easy to embed.

Why this design

  • Performance. Graph layout and geometry are arithmetic-heavy and branchy — exactly what compiles well to WASM and runs poorly as idiomatic JS.
  • Portability. Because nodus-core has no DOM dependencies, the same Rust powers the browser engine and native targets.
  • A stable, typed API. None of this leaks: you still write new Nodus(...), setGraph(...), layouts.force(...). The compiled core is an implementation detail.

Next