← writing

Rendering 3,000 nodes on a canvas without dropping frames

The knowledge graph view started in SVG because SVG is pleasant: each node is an element, you attach a click handler, the browser does hit-testing for you. It held up through demos. It fell over in production, somewhere north of 800 nodes, when panning turned into a slideshow.

The problem with SVG at that scale is that every node is a DOM element and every frame is a layout. Canvas has no DOM — you get one element and a 2D context, and you're responsible for everything, including working out what the user clicked on.

The redraw loop

The whole view is one <canvas> and a render function that runs on a requestAnimationFrame loop only when something changed — a pan, a zoom, a simulation tick. A dirty flag, not a constant 60fps burn.

function frame() {
  if (dirty) { draw(); dirty = false; }
  raf = requestAnimationFrame(frame);
}

Layout comes from a force simulation that's allowed to run for a fixed number of ticks and then freezes. A graph that keeps jittering forever is exhausting to look at and expensive to draw.

Hit-testing by hand

Clicks are the part SVG did for free. On canvas you keep the node positions in a plain array and, on click, walk it to find the nearest node within its radius. Three thousand entries is nothing for a linear scan at click time — this only becomes a quadtree problem if you're hit-testing every mousemove, which you can avoid by only doing it on click and on a throttled hover.

Culling

Don't draw what's off-screen. Before the draw loop, filter nodes and edges to the current viewport plus a margin. At high zoom you're drawing 3,000 nodes; zoomed in you might be drawing 40, and the frame cost drops with it.

The result renders the full 3,000-node graph and pans smoothly. The code is longer and less elegant than the SVG version. That's the trade.

← back to the notebook