Network graphs show relationships: entities as nodes, connections as edges. Supply chain dependencies, system architectures, organisational hierarchies, knowledge graphs. In 2D, they work well to a few hundred nodes. Beyond that, edges cross, nodes overlap, and the diagram becomes a tangle. The third axis gives the layout algorithm room to separate clusters and reduce crossings.
The trade-off is interaction complexity. Users must navigate 3D space to explore the graph. That overhead is justified when the relationship structure is dense enough that 2D fails. A supply chain with 500 suppliers and 3,000 dependencies is already unreadable in 2D. In 3D with community detection, clusters separate, bridge nodes become visible between them, and the single points of failure that were invisible in the spreadsheet become obvious.
Dependency Blast Radius
An NPM dependency tree for a typical web application: 47 packages across four domains. Click any package to see its blast radius, everything that breaks if it disappears. The small shared packages near the bottom often have wider reach than the direct dependencies at the top.
The Constraint: Two Expensive Problems at Once
A network graph with 10,000 nodes and 50,000 edges presents two problems simultaneously. Solving one while ignoring the other still produces an unusable application.
Layout Computation
Force-directed algorithms simulate physical forces: nodes repel, edges attract. Each iteration calculates forces between all node pairs. Naive: O(n2). With Barnes-Hut spatial partitioning: O(n log n). At 10,000 nodes, even the optimised version takes tens of milliseconds per iteration. On the main thread, the scene freezes while the layout settles.
The layout must run off the main thread. No exceptions.
Rendering
10,000 nodes as individual Mesh objects: 10,000 draw calls. 50,000 edges as individual Lines: 50,000 more. Combined with layout computation, the frame budget is spent several times over before a single pixel appears.
One InstancedMesh + one LineSegments = 2 draw calls. Same visual result.
These problems interact. If layout computation blocks the render loop, the scene freezes during settling. If rendering consumes the frame budget, there is no room for interaction. The architecture must handle both: layout off the main thread, rendering in minimal draw calls.
The Naive Approach
Tutorial-grade graph code works with 50 nodes. At 500 it is sluggish. At 5,000 the browser tab is unresponsive. These patterns are fine for demonstrations. They do not survive real relationship datasets.
The Robust Pattern: Layout in a Web Worker
Force-directed layout is CPU-intensive and iterative. Moving it off the main thread is the single most important architectural decision. The scene stays interactive while the layout settles. Users can orbit, zoom, and hover during computation.
Initialise worker with graph data: node IDs, edge source/target pairs, initial positions (random or from a previous layout).
Worker runs simulation using d3-force-3d or ngraph.forceLayout. Barnes-Hut approximation keeps each iteration under 10ms for 10K nodes.
Posts position updates to the main thread at 30Hz (not every iteration). A Float32Array via transferable objects avoids serialisation cost.
Main thread writes positions into the InstancedMesh matrix buffer and the LineSegments position buffer. Two buffer updates, two draw calls, 60fps.
The result: The main thread never blocks on layout. The scene remains responsive during settling. Users see the graph organise itself in real-time, clusters forming and separating as forces converge. Interaction works throughout.
For graphs under ~500 nodes (like the demo above), the simulation is cheap enough to run on the main thread in requestAnimationFrame. The Web Worker architecture becomes essential above that threshold, and it is worth implementing from the start if the dataset might grow.
Instanced Rendering: 10,000 Nodes, 2 Draw Calls
InstancedMesh renders N copies of the same geometry with one draw call. Per-instance attributes encode the visual differences: position from the layout, scale from node importance (degree centrality, value, or category), colour from community membership.
| Approach | Draw calls (10K nodes) | Hover detection | Visual flexibility |
|---|---|---|---|
| Individual Mesh | 10,000 | Native raycasting | Full per-node materials |
| InstancedMesh | 1-3 | Raycasting (fast on instances) | Per-instance colour + scale |
| Points | 1 | GPU picking required | Camera-facing circles only |
For edges, a single LineSegments geometry with a shared BufferAttribute for positions. Update the position buffer when the layout worker sends new coordinates. Vertex colours encode edge type or highlight state without requiring separate materials. One geometry, one draw call, handles 100K+ edges.
If you need three different node shapes (circles for suppliers, squares for hubs, diamonds for customers), use three InstancedMesh objects. Three draw calls is still trivially fast. The rendering cost is negligible compared to what individual meshes would require.
Interaction at Scale
A static graph is a picture. An interactive graph is a tool. Four interaction patterns cover most analytical needs, each with different performance characteristics.
Neighbourhood Highlighting
Click a node. Its direct connections light up. Everything else dims. The user sees exactly which entities this node depends on. In the supply chain graph, clicking a bridge node instantly reveals which clusters it connects and which product lines depend on it.
Implementation: update per-instance colour buffer. Dim = multiply RGB by 0.2. Bright = full colour. One buffer update per interaction.
GPU Picking
At 10,000+ nodes, raycasting against InstancedMesh is still fast (Three.js BVH handles it), but for extreme scale, GPU picking is constant-time. Render each node in a unique colour to an offscreen buffer. Read the pixel under the mouse. The colour is the node ID.
Cost: one extra render pass to a small offscreen target. Hover detection: O(1) regardless of node count.
Zoom-to-Node
Double-click a node and the camera smoothly animates to centre on it. Lerp camera position and lookAt target over 300-500ms. The user does not lose orientation because they see the transition. Provides focused examination of a specific neighbourhood.
Critical for large graphs where manual navigation would take too long to find a specific entity.
Sector Filtering
Filter by category, community, or attribute. Non-matching nodes dim to near-invisible. The graph's spatial layout is preserved (positions do not change) so the user maintains spatial context. The filter reveals substructure within the full layout.
Faster than re-running the layout for a subgraph, and preserves the user's mental model of the full graph.
Community Detection and Bridge Analysis
Colour-coding by community makes cluster structure visible immediately. But the most valuable insight is often not within clusters, it is between them: the bridge nodes.
This is the pattern that makes network graphs worth the investment. A procurement spreadsheet lists suppliers in rows. It does not show that the same small supplier appears in four different product lines' dependency chains, connecting clusters that otherwise have no overlap. The 3D graph, with bridge nodes highlighted in red and everything else dimmed, makes that structural risk visible at a glance. The spreadsheet had the data all along. It just could not show the shape.
Advanced Patterns
Beyond the core rendering and interaction, four patterns address specific requirements for complex network analysis.
Hierarchical Graphs
Networks with hierarchy (org charts, file systems, taxonomies) use radial or layered layouts instead of force-directed. In 3D, each level occupies a different height, creating a readable spatial tree. Force-directed layouts obscure hierarchy; purpose-built layouts reveal it.
Dynamic Graphs
Graphs that change over time (live network traffic, evolving supply chains) need incremental layout. Fix existing node positions. Let only new nodes seek equilibrium. Use transition animations so users track what moved and what appeared.
Edge Bundling
Dense graphs produce visual clutter from overlapping edges. Edge bundling routes similar paths together, like a cable run. Compute in the Web Worker alongside layout. Reduces visual noise without removing data.
50K+ Node Strategies
At extreme scale: LOD for nodes (render clusters as single large spheres when zoomed out, expand to individuals when zoomed in), edge filtering (show only the strongest N connections per node), progressive loading (core nodes first, periphery on demand).
Libraries
Three libraries provide foundations at different levels of abstraction. The choice depends on how much control you need over rendering and interaction.
For production applications with custom interaction requirements, the combination of d3-force-3d (layout in a Worker) with custom Three.js rendering (InstancedMesh + LineSegments) provides the best balance of performance, control, and visual quality.
The Business Link
Network graphs reveal relationship structure that tables and lists cannot show. A procurement spreadsheet lists suppliers. A network graph shows which suppliers are single points of failure. An org chart shows reporting lines. A network graph shows where communication actually flows (and where it bottlenecks). A customer database lists accounts. A network graph shows influence patterns and referral chains.
The investment in 3D graph infrastructure pays back when the relationship data is dense enough that 2D becomes unreadable. For most organisations, that threshold is lower than expected: 500 entities with 2,000+ connections is already a tangle in 2D. The third axis, combined with community detection and interactive filtering, turns that tangle into actionable structure.
Visualise Relationships
We build network graph visualisations that reveal structure in complex relationship data. Force-directed layouts, instanced rendering, community detection, bridge analysis, interactive exploration: scaled to the size where patterns become visible.
Let's talk about your graph data →