rbush
RBush is an in-memory JavaScript spatial index for two-dimensional points and rectangles. It stores each item by its minimum and maximum X and Y coordinates in an optimized R-tree, then returns items whose bounding boxes intersect a query box. The API supports single insertion and removal, faster bulk loading, collision checks, complete traversal, custom item shapes, and JSON export and import. It runs in modern browsers and Node and is commonly used behind maps, hit testing, labels, and visualization interactions.
RBush is the small, proven choice for mutable rectangle intersection queries in JavaScript. Use a static index for immutable data, add exact geometry filtering when boxes are only approximations, and avoid it if ESM or separate community types are deal-breakers.
Use it if
- You repeatedly query which mutable points or rectangles intersect a viewport, tile, selection box, or cursor region
- Your items can be represented by axis-aligned bounding boxes and exact geometry checks can happen after the index narrows candidates
- You need incremental insertion and removal, not just one immutable build
- You want to build an index on a server, serialize it, and search the same structure in a browser
- You index a static set of points only: RBush's own README recommends KDBush and says its point indexing is 5 to 8 times faster for that case
- You need nearest-neighbor queries: the core API exposes bounding-box search and collision checks, while nearest neighbors require the separate rbush-knn package
- You need exact polygon, line, distance, or geographic wraparound predicates: RBush only compares axis-aligned Cartesian bounding boxes and can return geometric false positives
- Your runtime is CommonJS: the compatibility section says RBush 4 and newer is published as an ES module and no longer supports CommonJS environments
- You require first-party TypeScript declarations: the 4.0.1 package files contain JavaScript bundles but no declaration files, so TypeScript projects rely on the separate @types/rbush package
Setup reality
Install `rbush` and import its default export from ESM. Version 4.0.1 declares `type: module`; `require('rbush')` is not a supported v4 path. TypeScript users need `@types/rbush`, maintained separately on DefinitelyTyped, so type releases can lag runtime changes. The default item shape is exactly `{ minX, minY, maxX, maxY }`; extra fields are preserved, but coordinate validation is your job. Normalize reversed bounds, NaN, infinities, longitude wraparound, and coordinate reference systems before insertion. Search returns bounding-box intersections, not exact geometry matches, so run a second predicate for circles, polygons, or rotated objects. The optional constructor value controls maximum entries per node and defaults to 9. The README says higher values favor insertion speed at the cost of search speed; benchmark real spatial distributions before changing it. For initial datasets call `load()` once because the documented bulk path is faster and builds a better query tree than repeated insertions. Repeatedly loading scattered batches into an existing tree can worsen queries because each batch becomes a separate small tree before merging. Removal uses object identity by default. Keep the original inserted object or pass an equality function when removing a reconstructed copy. Mutating an item's coordinates after insertion leaves it stored under stale ancestors; remove it before mutation and insert it again afterward. JSON is the internal tree, not just an item list, and importing requires the same node size used for export. The package does not persist, synchronize, lock, or update across workers. Treat each instance as mutable process-local state, and rebuild or transmit snapshots when authoritative data changes.
Patterns
Insert one rectangle with application datainsert-rectangle
import RBush from 'rbush'
const tree = new RBush()
const marker = {
id: 'marker-42',
minX: 20,
minY: 40,
maxX: 30,
maxY: 50,
}
tree.insert(marker)Coordinates are not validated. Ensure min values do not exceed max values and reject NaN before insertion.
Build an index from an initial datasetbulk-load-items
const tree = new RBush(9)
tree.load(features.map(feature => ({
minX: feature.bounds.left,
minY: feature.bounds.top,
maxX: feature.bounds.right,
maxY: feature.bounds.bottom,
feature,
})))Bulk loading an empty tree is preferable to repeated insert calls. Loading scattered batches into an existing tree can reduce query quality.
Find items intersecting a viewportsearch-bounding-box
const visible = tree.search({
minX: viewport.left,
minY: viewport.top,
maxX: viewport.right,
maxY: viewport.bottom,
})Results intersect the query box; they do not have to be fully contained. Apply an exact geometry predicate after search when necessary.
Stop after finding the first intersectioncheck-any-collision
const occupied = tree.collides({
minX: candidate.x,
minY: candidate.y,
maxX: candidate.x + candidate.width,
maxY: candidate.y + candidate.height,
})collides returns only a Boolean and can finish earlier than search. It still uses bounding boxes, not shape-level collision detection.
Remove an item by object identityremove-original-item
const item = { id: 7, minX: 0, minY: 0, maxX: 10, maxY: 10 }
tree.insert(item)
// Later, keep and use the same object reference.
tree.remove(item)The default removal comparison is reference identity. An equivalent object literal will not remove the inserted item.
Remove a reconstructed item with an equality functionremove-item-by-id
const copy = { id: 7, minX: 0, minY: 0, maxX: 10, maxY: 10 }
tree.remove(copy, (a, b) => a.id === b.id)The copy's bounding box still guides the tree traversal. If its coordinates differ from the inserted item, the equality callback may never see it.
Move an indexed item safelyupdate-item-bounds
tree.remove(item)
item.minX += dx
item.maxX += dx
item.minY += dy
item.maxY += dy
tree.insert(item)Do not mutate indexed bounds in place. Ancestor boxes are not automatically updated, which can make later searches miss the item.
Index point arrays with a subclassindex-custom-points
class PointBush extends RBush {
toBBox([x, y]) {
return { minX: x, minY: y, maxX: x, maxY: y }
}
compareMinX(a, b) { return a[0] - b[0] }
compareMinY(a, b) { return a[1] - b[1] }
}
const points = new PointBush().load([[20, 50], [10, 30]])Search queries still use the standard minX, minY, maxX, maxY box. For immutable points, the README recommends KDBush instead.
Serialize an index for another processexport-index-json
const nodeSize = 16
const tree = new RBush(nodeSize).load(items)
const payload = JSON.stringify({ nodeSize, tree: tree.toJSON() })Store nodeSize with the tree. fromJSON must use the same constructor setting for correct behavior.
Restore a serialized indeximport-index-json
const payload = JSON.parse(serialized)
const tree = new RBush(payload.nodeSize).fromJSON(payload.tree)
const matches = tree.search(queryBox)Treat imported JSON as untrusted input at security boundaries. RBush does not validate the entire tree structure before using it.
Read all items and reset the indexlist-and-clear-items
const snapshot = tree.all()
console.log(`indexed: ${snapshot.length}`)
tree.clear()
console.log(tree.all().length) // 0all traverses the whole index and allocates a result array. It is not a cheap count operation for large trees.
Benchmark a nondefault node sizechoose-node-size
for (const nodeSize of [9, 16, 32]) {
const tree = new RBush(nodeSize).load(items)
const started = performance.now()
for (const box of representativeQueries) tree.search(box)
console.log(nodeSize, performance.now() - started)
}The default is 9. The README says higher values favor insertion speed but slow searches, so benchmark representative data and queries.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| flatbush | npm | Your rectangle index is static after construction and you want a compact packed representation |
| kdbush | npm | You have a static set of points and want faster point indexing with range and radius queries |
| @turf/geojson-rbush | npm | Your data is GeoJSON and you want Turf-aware bounding boxes and feature collection helpers |
| rbush-knn | npm | You already use RBush but also need k-nearest-neighbor queries around a point |