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 IDsedges— 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:
| Field | Description |
|---|---|
path_node_set | Nodes on the path/MST |
path_edge_set | Directed edge keys "u-v" (bidirectional) |
mst_edge_set | MST edge keys (for Kruskal) |
path_array | Path 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:
- Degree-based (default):
ratio = degree/max_degree→#0891b2(leaf) →#22d3ee(mid) →#f59e0b(hub) →#f97316(super-hub) - PageRank: Normalizes scores to 0..1; maps to cyan→violet gradient (4 buckets)
- SCC: 7-color palette rotated by component index
- 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-awaitto the main buildvite-plugin-wasmwithworker.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.