Skip to main content
Version: 4.3

Performance

Large diagrams strain an app in two different ways. Query cost grows with every cell the app has to walk through when it looks something up by location: what is inside this region, what sits under this point. Rendering cost grows with every cell the browser has to keep in the DOM. @joint/react-plus gives you a lever for each. The spatial index keeps location queries fast, and virtual rendering keeps the DOM small. They are independent, so you can enable either one on its own, but they work best together.

Spatial index​

Location queries answer questions like "which elements are inside this rectangle" or "which elements lie under this one". A plain graph answers them by checking every cell, which is fine for a small diagram but gets slower as it grows. The spatialIndex prop on <Diagram> backs the graph with a quad-tree index instead, so those lookups only visit the region they ask about:

<Diagram initialCells={initialCells} spatialIndex>
...
</Diagram>

Turning it on already speeds up the framework's own hit-testing on large graphs: region selection, drop-based embedding, and link snapping all run location queries internally, and they pick up the index without any query code on your side.

Pass true for the defaults, or an options object to tune the quad-tree. See the tuning section below.

Querying from your own code​

The index pays off most when your own features ask spatial questions. The find methods are part of the regular graph API, so the code does not change at all: reach the graph with useGraph() from any component inside the diagram and call them. With spatialIndex on, every call hits the quad-tree instead of walking the whole graph.

import { useGraph } from '@joint/react-plus';

const { graph } = useGraph();

// Elements intersecting a rectangle, in graph coordinates.
const inArea = graph.findElementsInArea({ x: 0, y: 0, width: 500, height: 500 });

// Elements whose bounding box overlaps another element's.
const covered = graph.findElementsUnderElement(element);

Each find method has an at-point variant and link and cell counterparts. The full list lives in the SearchGraph API.

The demo below runs findElementsUnderElement on every pointer move while you drag the dashed probe across a field of 1,200 cards. Cards light up the moment the probe covers them, and the badge in the top right corner tracks the live count:

Tuning​

The index keeps itself up to date in one of two modes. In eager mode, the default, every graph change updates the quad-tree right away, so queries always hit a current index. In lazy mode, changes only mark the index as stale, and the next query rebuilds it in one pass. Eager mode suits diagrams where queries are frequent, like hit-testing on every pointer move. Lazy mode suits diagrams that change in bursts, like a big import followed by an occasional lookup, because it pays the indexing cost once instead of on every change:

<Diagram initialCells={initialCells} spatialIndex={{ isQuadTreeLazy: true }}>
...
</Diagram>

The options object accepts a few more knobs, all reactive, so changing a value reconfigures the live index:

  • quadTreeMaxDepth caps how deep the tree subdivides. A deeper tree localizes queries to smaller regions at the cost of more nodes.
  • quadTreeCapacity sets how many elements a tree node holds before it splits.
  • quadTreeBoundary hands the index a fixed bounding box to cover, instead of letting it size itself around the content.
  • isQuadTreeAutoGrow lets that fixed boundary expand when elements land outside it.

The defaults serve most diagrams well. Reach for these only when profiling points at the index itself.

Virtual rendering​

With virtual rendering enabled, the scroller renders only the cells inside the current viewport and skips the rest. Cells mount as they scroll into view and unmount as they leave, so the DOM stays small no matter how many cells the graph holds.

<PaperScroller virtualRendering>
<Paper renderElement={renderElement} />
</PaperScroller>

Virtual rendering is a <PaperScroller> feature: the visible viewport decides which cells to render, so it has no effect on a plain <Paper> without a scroller around it.

Pass true for the defaults, or an options object to tune the behavior. The most useful knob is margin, which inflates the culling area by the given number of pixels so cells near the edge mount before they scroll into view:

<PaperScroller virtualRendering={{ margin: 300 }}>
<Paper renderElement={renderElement} />
</PaperScroller>

The demo below holds 10,000 cards. Watch the badge in the top right corner: at any moment the paper renders only the handful of cards inside the viewport. Drag the canvas, zoom, or jump to a random spot, and the count stays low while cards mount and unmount around you.

Pair it with the spatial index

On every scroll and zoom, virtual rendering asks the graph which cells intersect the viewport. That question is one of the location queries the spatial index accelerates, which is why the demo above enables both: the culling check stays fast even at 10,000 cards.

Stay in the know

Be where thousands of diagramming enthusiasts meet

Star us on GitHub