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
ResizeObserverand 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:
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()
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 underResizeObserver, 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 bymarginpx (default64) and by room for the curve. A culled connection stays in the model, gets nolayoutentry, and is drawn again when it comes back.maxConnectionsForJumps—number· default:150. When moresmoothstepconnections 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.
| Scenario | 0.4.3 | 0.4.5 | Limit |
|---|---|---|---|
| 100 blocks, 100 connections, median / p95 | 2.1 / 2.8 | 0.7 / 1.3 | ≤ 4 / ≤ 8 |
| 300 blocks, 300 connections, median | 5.7 | 2.2 | ≤ 12 |
| FPS dragging a block with 20 lines, 300 blocks | 59.8 | 60.6 | ≥ 55 |
| FPS scrolling columns of 200 blocks, 100 lines | 60.9 | 60.3 | ≥ 55 |
| First render, 1000 blocks, 500 connections | 47 | 39 | ≤ 400 |
| 1000 blocks without connections: observed elements | 1001 | 1 | 1 |
| Heavy styling¹, 100 / 100, median | 10.0 | 2.6 | — |
| Heavy styling¹, 300 / 300, median | 41.0 | 5.9 | — |
| Heavy styling¹, 300 / 300, FPS dragging | 13.5 | 48.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.culland keepclipToScrollParentson. - Prefer
bezierorstraightfor many lines;smoothstepwith 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.