Performance
The repository ships micro-benchmarks for every hot path (bun run bench at the root). They run on deterministic fixtures, warm up for 100ms, sample for 500ms, and report median, p95, and throughput. A budget.json of medians times 2.5 guards CI against algorithmic blow-ups.
Indicative numbers
Taken on a desktop CPU (Intel Core i9-12900KF under WSL2, Bun 1.3.14). Expect different absolute values elsewhere; the ratios are what matter.
Nested: flattenLayout of an 85-node tree takes 0.4 ms and of a 341-node tree 1.1 ms; transferItem between two 8-item containers 0.027 ms.
Scaling
Growth from 128 to 512 items (linear would be 4x, quadratic 16x, cubic 64x):
placeItempointer form: about 64x. The fallbacks re-solve against every sibling for every candidate. Pointer drops into dense layouts above roughly 100 items will be noticeable; use the position form or pre-compute a slot when you can.moveItemfree drop: about 28x. Even a drop in open space runs the reorder, swap, and insert attempts first, each scanning all siblings.moveItempush cascade: about 19x, quadratic in the chain length.applyGap, chain projection, segment projection: 5-12x, between linear and quadratic. Chain projection is consistently 2-3x the cost of segments on the same input.resizeItem,compactLayout,flattenLayout: linear.
In practice: a dashboard of 30-60 items solves in well under a millisecond per pointer move on a desktop CPU and comfortably on a phone. Hundreds of items in one canvas need care; thousands should be split into nested containers, which keeps each solve local.
Keeping interactions smooth
- Solve on the rendered layout, once per pointer move. That is what the adapter does. Do not solve on both pointer move and animation frame.
- Position with
transform.GridItemdoes by default; it keeps layout work off the main thread. Siblings can transitiontransform,width, andheightin 120-220ms; the active item should not transition at all. - Subscribe narrowly.
useGridItemViewrerenders an item only when its own rect or flags change. AvoiduseGridVisibleLayoutin per-item components. - Contain the canvas.
contain: layout painton the canvas element (with an intrinsic-size fallback) limits style and layout work to the canvas during a drag. - Prefer segments for sparse layouts when you project often and do not need exact gaps.
- Throttle projection to animation frames if you project on every
ResizeObservercallback with very large layouts. - Nest. A nested container solves only its own children.
flattenLayoutis linear, so deep trees are cheap to render.
Measuring your own case
bun run benchmarks/run.ts --filter moveItem runs a subset; --json results.json writes results; --check compares against the budget. The fixtures (dashboardLayout, packedLayout, collisionChainLayout, nestedTree, and more) are seeded, so numbers are comparable between commits on one machine. See benchmarks/README.md in the repository for the full table and the fixture shapes.