mrkeyoor.com_
Sat 08 Aug 20:59 UTC
npmDataupdated 08 Aug 2026

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.

Verdict

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.

API stability4/5The core constructor, insert, load, search, collides, remove, clear, all, toJSON, and fromJSON API is compact and longstanding. The notable compatibility break is packaging: the README states that v4 and newer are ESM-only and drop CommonJS. Internal JSON is exposed for transfer, but callers must preserve matching node size and should not treat its private shape as a general database format.
Docs5/5The README explains item shape, node-size tradeoffs, identity-based removal, custom data subclasses, bulk-load strengths and weaknesses, search semantics, JSON transfer, nearest-neighbor add-ons, algorithms, and benchmark context. It directly recommends KDBush for static points and documents ESM-only compatibility, giving readers unusually clear reasons not to use RBush.
Maintenance4/5GitHub reports a push on 2026-07-21 and only 12 open issues and pull requests. npm 4.0.1 was published in August 2024, so releases are infrequent, but the repository continues to receive current maintenance and uses modern Node tests and lint tooling. The small runtime consists of one main source file plus quickselect, reducing dependency-related maintenance exposure.
Ecosystem4/5RBush recorded 5,444,239 downloads for the week ending 2026-08-06 and has 2,765 GitHub stars. Companion packages add nearest-neighbor and GeoJSON handling, while browser bundles and ESM support server-to-client index transfer. First-party TypeScript types and geographic primitives are absent, so those ecosystem links depend on DefinitelyTyped or separate wrappers.

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
Skip it if

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) // 0

all 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

PackageRegistryPick it when
flatbushnpmYour rectangle index is static after construction and you want a compact packed representation
kdbushnpmYou have a static set of points and want faster point indexing with range and radius queries
@turf/geojson-rbushnpmYour data is GeoJSON and you want Turf-aware bounding boxes and feature collection helpers
rbush-knnnpmYou already use RBush but also need k-nearest-neighbor queries around a point