Good question. Let me break down what D3 is actually doing here and whether it’s the right tool for each role.
What D3 provides today Role D3 feature used Alternatives Layout d3.forceSimulation Custom physics (you’re already halfway there with GraphPhysics) Rendering SVG data joins (enter/update/exit) Canvas 2D, WebGL (via regl/pixi/deck.gl), or raw DOM Zoom/Pan d3.zoom Any pointer math library, or hand-rolled Hulls d3.polygonHull Trivial convex hull algo (~30 lines) Drag d3.drag Native pointer events Transitions d3.transition RAF interpolation (you already do this in 13+ loops) Where D3 is actively hurting SVG at this scale. The review documents 6 SVG layer groups, per-edge gradient defs, arrowhead markers, hull polygons, textPath labels – all as DOM nodes. Every renderPositions() call touches hundreds of DOM elements. With 13+ RAF loops calling renderPositions(), you’re doing redundant DOM writes every frame. SVG hit-testing and style recalc costs grow linearly with element count.
Data joins for a streaming graph. D3’s enter/update/exit pattern assumes batch reconciliation. Your renderIncremental already works around this, and the planned WebSocket streaming will make incremental updates the norm, not the exception.
Force simulation vs GraphPhysics. You’ve already built GraphPhysics as a separate IIFE with spatial grid acceleration, type-weighted edges, settle detection, and hull throttling. D3’s force sim is used only in layoutClusterInternal (180 iterations) and packClusters (120 iterations) – both one-shot computations that could be inlined.
Zoom behavior coupling. d3.zoom gives you a transform, but applySemanticZoom immediately bolts navigation semantics onto it. The review identifies this as the core architectural problem. D3’s zoom abstraction isn’t helping – it’s a thin wrapper over pointer events that then gets overloaded.
What I’d recommend Switch rendering to Canvas 2D. This is the single highest-impact change:
One renderPositions() call redraws everything in a single pass – no DOM diffing Hit-testing via spatial index (you already have one in GraphPhysics) instead of SVG event bubbling Gradients/arrowheads become draw calls, not DOM elements 10-100x faster for graphs of this density Still 2D, no WebGL complexity Keep D3 only for d3.polygonHull and d3.scaleOrdinal (color scales). These are pure math utilities with no rendering opinion. Everything else can go.
Don’t use WebGL/regl/pixi unless you’re targeting 10k+ nodes. Canvas 2D handles thousands of nodes at 60fps easily, and the code stays simple – no shader programming, no GPU buffer management. The planned expand/collapse means you’re showing fewer nodes at any given time, not more.
How this fits the plan The review’s recommended file split already points this direction:
core/animation.js (central RAF scheduler) eliminates redundant draws – Canvas makes this a single ctx.clearRect() + redraw render/positions.js becomes a Canvas draw loop instead of DOM transforms navigation/semantic-zoom.js returns a NavigationState that the Canvas renderer consumes – clean separation Streaming incremental updates just mutate the node/link arrays and the next frame picks them up – no data joins The transition path: You don’t need to rewrite everything at once. Canvas and SVG can coexist in the same page. Move node circles and edges to Canvas first (biggest win), keep hulls and labels in SVG temporarily (text rendering is easier in SVG), then migrate those too.
Short answer: D3 was the right choice for a prototype. For the architecture described in plan2 – streaming mutations, physics-driven layout, expand/collapse topology changes – Canvas 2D with D3 used only as a math library is the better fit.