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.