Skip to main content

WebAssembly Computation Layer

Motivation​

Graph files can be large — hundreds of thousands of edges. Parsing them in JavaScript using split/regex/parseInt is slow and generates significant GC pressure. Beyond parsing, rendering 3,000 nodes requires computing fill colors, sizes, and adjacency lookups on every React render cycle.

The solution: compile a Rust crate to WebAssembly using wasm-pack, targeting the bundler output format for Vite integration. The WASM module (128 KB, gzipped to 59 KB) runs inside a Web Worker, keeping the main thread free.

Integration Architecture​

App.tsx
│
├─ uploads file
│
▼
Web Worker (parseGraph.worker.ts)
│
├─ imports parseGraph.ts
├─ awaits wasmReady (loads graph_wasm.js)
├─ calls wasm.parse_edge_list(content) ← WASM: parse + build adjacency
├─ deserializes JSON result
└─ postMessage({ ok: true, result: ParsedGraph })

GraphView.tsx (React component)
│
├─ receives parsedGraph.neighborMap (pre-computed, no JS work)
│
├─ useMemo: build_path_sets via WASM → pathNodeSet, pathEdgeSet, mstEdgeSet
│
├─ useMemo: compute_node_styles via WASM → graphNodes[] for Reagraph
│
├─ useMemo: compute_edge_styles via WASM → graphEdges[] for Reagraph
│
└─ GraphCanvas (Reagraph/Three.js/WebGL) → renders

The WASM module is loaded once (cached) inside the Web Worker. In GraphView, getWasm() checks the cached module pointer; if WASM is not yet loaded, a JS fallback runs synchronously — the UI never blocks.

Exported Functions​

parse_edge_list(content: &str) → String​

Parses a whitespace-separated edge list file (2 or 3 columns: u v [weight]). Skips comment lines (#, %).

Returns JSON with:

  • nodes — array of unique node IDs
  • edges — array of {source, target, weight}
  • adjacency_map — {nodeId: [neighborId, ...]}
  • total_nodes, total_edges — counts (may exceed display limit)
  • truncated — boolean flag if >3,000 display nodes

parse_adjacency_list(content: &str) → String​

Parses adjacency list format: node: neighbor1,weight1 neighbor2 …

Returns the same JSON schema as parse_edge_list. Both parsers build the neighbor adjacency map in the same pass as edge collection, avoiding a second traversal — for a 100K-edge graph, this eliminates an extra O(E) loop.

build_neighbor_map(edges_json: &str) → String​

Standalone function. Takes a JSON array of {source, target} objects, returns {nodeId: neighborId[]}. Used when the graph is already parsed but adjacency needs recomputing (e.g., after filtering).

build_path_sets(path_json: &str, algorithm: &str) → String​

Takes the result path array (JSON number array) and algorithm name. Returns four sets:

FieldDescription
path_node_setNodes on the path/MST
path_edge_setDirected edge keys "u-v" (bidirectional)
mst_edge_setMST edge keys (for Kruskal)
path_arrayPath as string array

For Kruskal, the path array is interpreted as interleaved [u0, v0, u1, v1, ...] pairs. This handles the special MST output format without branching in JavaScript.

compute_node_styles(...) → String​

The most computationally significant frontend function. Computes fill color and size for every node in a single WASM call. Handles four coloring modes:

  1. Degree-based (default): ratio = degree/max_degree → #0891b2 (leaf) → #22d3ee (mid) → #f59e0b (hub) → #f97316 (super-hub)
  2. PageRank: Normalizes scores to 0..1; maps to cyan→violet gradient (4 buckets)
  3. SCC: 7-color palette rotated by component index
  4. Path overlay: Path nodes → cyan; start node → green; A* end node → red

All of this runs in a tight Rust loop without heap allocations per node — no per-object GC cost.

compute_edge_styles(edges_json, path_edge_set_json, mst_edge_set_json) → String​

Computes fill and size for every edge:

  • Path edges → cyan / size 2
  • MST edges → green / size 2
  • Default → dark blue / size 0.7

Uses HashSet lookups for O(1) edge membership tests.

Build Configuration​

The WASM crate (wasm/Cargo.toml) is compiled with:

[profile.release]
opt-level = "s" # optimize for size
lto = true # link-time optimization

[lib]
crate-type = ["cdylib"]

The Vite config applies:

  • vite-plugin-wasm + vite-plugin-top-level-await to the main build
  • vite-plugin-wasm with worker.format: "es" to the worker build

Worker ES module format is required because WASM code-splitting produces multiple chunks that IIFE format cannot handle.

JSON as API Boundary​

WASM functions accept and return JSON strings rather than typed arrays or shared memory. This trades raw performance for:

  • Simplicity — no manual memory management, no pointer arithmetic in JS
  • Compatibility — works with any JSON-serializable data structure
  • Debuggability — output is inspectable in DevTools

For the workloads in this system (3,000 nodes, ~10,000 edges), JSON serialization overhead is <2ms — acceptable. For larger graphs, the API could be migrated to use SharedArrayBuffer or typed array views.