Performance
Nodus is built for scale. The Rust/WebAssembly core holds the graph and runs layouts off the main thread, while the GPU renderer draws everything with instanced rendering — one draw call for all nodes, one for all edges. The result: tens of thousands of elements that stay smooth to pan, zoom and spin.
The demo below builds a scale-free graph (hubs emerge, like a real network), lays it out, and frames it. Switch the renderer (WebGPU, WebGL2 or Canvas 2D) and the node count, re-run the layout, or hit Spin to rotate the camera continuously and read the live frame rate — flip between renderers while spinning to compare them at the same scale.
Building a large graph fast
Section titled “Building a large graph fast”Graphs this size are cheapest to build by hand — a plain { nodes, edges }
object. Here each new node attaches to a couple of existing nodes chosen by
preferential attachment (degree-weighted), so a few hubs accumulate most of
the links. Generating 50,000 nodes this way takes only a few milliseconds.
function buildGraph(N, M = 2) { const nodes = []; const edges = []; const endpoints = []; // node ids repeated by degree → degree-weighted picks for (let i = 0; i < N; i++) { nodes.push({ id: i }); if (i === 0) continue; const picked = new Set(); for (let k = 0; k < Math.min(M, i); k++) { let t; do { t = endpoints.length ? endpoints[(Math.random() * endpoints.length) | 0] : 0; } while (picked.has(t)); picked.add(t); edges.push({ source: i, target: t }); endpoints.push(i, t); // bump both endpoints' degree weight } } return { nodes, edges };}
await nodus.setGraph(buildGraph(50000));Prefer a named topology? nodus.generate.* ships barabasiAlbert, erdosRenyi,
randomTree, grid and more — see Generating Graphs.
Laying it out
Section titled “Laying it out”The force layout is a Barnes–Hut approximation (the theta option trades
accuracy for speed). Two engines back it:
- CPU — a Rust/WebAssembly simulation that runs off the main thread in a worker, so the page stays responsive while nodes settle.
- GPU (
gpu: true) — a WebGL2 simulation that steps the whole particle system on the GPU. It is ~20× faster at scale (50,000 nodes lay out in ~2 s instead of ~40 s) and falls back to the CPU engine automatically when WebGL2 is unavailable. The demo uses it for its larger tiers (≥25k), where it matters; smaller graphs stay on the robust off-main-thread CPU engine.
Pass duration: 0 to jump straight to the settled positions — best for large
graphs — then frame the result.
await nodus.layouts.force({ duration: 0, gpu: true });await nodus.view.locateGraph();Layout is a one-time cost that grows with the graph; rendering and interaction
stay fast regardless. If the user switches scale mid-run, nodus.layouts.stop()
cancels the in-flight layout.
Keeping a dense graph legible
Section titled “Keeping a dense graph legible”At 50,000 nodes everything overlaps, so scale the visuals down as the graph grows — smaller radii and thinner, fainter edges keep the structure readable instead of a solid blob. Function-valued rules let one rule set adapt to the current scale without being re-added:
let style = { radius: 2.2, edgeWidth: 0.5, edgeColor: '#64748b' };
nodus.styles.addNodeRule({ radius: () => style.radius, color: '#2563eb' });nodus.styles.addEdgeRule({ width: () => style.edgeWidth, color: () => style.edgeColor });Comparing renderers
Section titled “Comparing renderers”Nodus picks a renderer for you, but you can choose one explicitly. The renderer
option takes 'wasm' — the Rust/wgpu GPU renderer, which prefers WebGPU and
falls back to WebGL2 — or 'canvas' for the Canvas 2D renderer:
const nodus = new Nodus({ container, renderer: 'wasm' }); // GPU (WebGPU → WebGL2)// const nodus = new Nodus({ container, renderer: 'canvas' }); // Canvas 2DThe demo’s WebGPU / WebGL2 / Canvas 2D buttons let you feel the difference.
Switching renderers re-creates the instance but reuses the laid-out positions,
so it redraws the same picture instantly instead of paying for the layout again —
read each node’s position, then hand them straight back as x/y attributes:
// snapshot the current layoutconst laidOut = { nodes: [...nodus.getNodes()].map((n) => { const { x, y } = n.getPosition(); return { id: n.getId(), attributes: { x, y } }; }), edges,};// ...re-create on another renderer, then redraw with no layout step:await nextNodus.setGraph(laidOut);await nextNodus.view.locateGraph();Use isWebGpuSupported() to detect WebGPU up
front — the demo disables the WebGPU button when it is unavailable. See
Backends & Targets for what each backend ships.
Measuring frame rate
Section titled “Measuring frame rate”The Spin button rotates the camera every frame and counts how many frames land per second — a direct read on rendering throughput at the current element count. Spin under each renderer to compare them: WebGPU is typically the fastest, with WebGL2 close behind and Canvas 2D trailing as the graph grows.
function spin() { nodus.view.rotate(0.004); // a small camera rotation each frame requestAnimationFrame(spin);}How far it scales
Section titled “How far it scales”Layout and rendering scale differently, and it helps to treat them separately:
- Rendering is the GPU’s job and scales furthest — the instanced renderer draws 100,000+ nodes and stays interactive to pan and zoom.
- Force layout is the heavier step. The GPU engine (
gpu: true) keeps it practical to ~50,000 nodes (the demo’s ceiling); past that, lay the graph out once with a cheaper layout (e.g.nodus.layouts.grid()) or precompute positions and pass them asx/yattributes, skipping force entirely.
In other words, if you already know where nodes go, Nodus will render far more of them than it will freshly force-lay.
See also
Section titled “See also”- The Rendering Pipeline — instanced GPU drawing, WGSL shaders and the Canvas 2D fallback.
- Architecture: Rust & WebAssembly — the core that holds the graph and runs layouts.
- Layouts — every layout and its options.