Skip to content

Performance ​

The engine is built for hundreds of blocks and connections, and for blocks that come and go — collapsed tree nodes, rows of a virtualized list.

What it does for you ​

  • Only what changed is redrawn. A pass measures the blocks, then rewrites the lines whose geometry or look actually changed. The paths and DOM nodes of the others are reused. Dragging a block with 20 lines among 300 touches those 20.
  • Reading and writing are separate. Every rectangle is measured once per frame, before anything is written to the DOM; label sizes are cached. No forced layout in the middle of a pass.
  • A block without connections costs nothing. It is not observed with ResizeObserver and not measured. It is registered when the first connection appears (or when it can start or end one with the connect mode).
  • The output does not change. With the default settings the SVG is byte-for-byte what 0.4.3 drew.

Batching ​

batch(fn) runs fn and draws once afterwards, however many blocks, connections or config changes it makes:

ts
linker.batch(() => {
  for (const id of expanded) linker.addConnection(links[id])
  linker.setConfig({ lines: { width: 3 } })
})

<VisualLinker> and useVisualLinker() already batch their own updates: one registry update per tick for directive-marked blocks, and only the changed connections are passed to the engine.

getStats() ​

ts
linker.getStats()
// { blocks, connections, rendered, culled, proxied, lastRenderMs, medianRenderMs, observedElements }
  • blocks, connections — how many are registered.
  • rendered — how many connections were written to the DOM in the last pass.
  • culled, proxied — hidden by culling, drawn to a fallback block.
  • lastRenderMs, medianRenderMs — time of the last pass, median over the last 60.
  • observedElements — elements under ResizeObserver, the container included.

It measures nothing in the DOM, so it is safe to call every frame. In Vue, stats() is exposed by the component and returned by useVisualLinker(); it is not reactive.

The performance group ​

  • cull — boolean | { margin?: number } · default: false. Connections that cannot be seen are not drawn: neither end nor the stretch between them reaches the visible area (the container, its scrolling ancestors and the window), widened by margin px (default 64) and by room for the curve. A culled connection stays in the model, gets no layout entry, and is drawn again when it comes back.
  • maxConnectionsForJumps — number · default: 150. When more smoothstep connections ask for jumps, the hops are switched off: finding crossings grows with the number of segments.
  • maxRoutingNodes — number · default: 6000. The size of the search grid of obstacle routing above which a line keeps its plain route.
  • maxObstacleBlocks — number · default: 200. When more blocks than this lie around a connection, it is not routed around them.

Measured ​

pnpm test:perf in the repository runs the stand playground/bench.html in Chromium and writes docs/PERFORMANCE.md; it fails when a limit is exceeded. Chromium, headless, a laptop, no throttling; times in ms, one refresh() with nothing changed unless noted.

Scenario0.4.30.4.5Limit
100 blocks, 100 connections, median / p952.1 / 2.80.7 / 1.3≤ 4 / ≤ 8
300 blocks, 300 connections, median5.72.2≤ 12
FPS dragging a block with 20 lines, 300 blocks59.860.6≥ 55
FPS scrolling columns of 200 blocks, 100 lines60.960.3≥ 55
First render, 1000 blocks, 500 connections4739≤ 400
1000 blocks without connections: observed elements100111
Heavy styling¹, 100 / 100, median10.02.6—
Heavy styling¹, 300 / 300, median41.05.9—
Heavy styling¹, 300 / 300, FPS dragging13.548.6—

¹ smoothstep, obstacle routing, jumps, a label and an arrow on every line, shown spread ports. The default look already met the targets in 0.4.3; the gain is where the expensive modes are on.

Tips ​

  • On big diagrams switch on performance.cull and keep clipToScrollParents on.
  • Prefer bezier or straight for many lines; smoothstep with routing and jumps is the costly combination.
  • Collapse trees with fallback ends instead of removing and re-adding connections.
  • Group changes with batch() when you use the engine directly.