RBush: a JavaScript R-tree index for points and rectangles
RBush — a high-performance JavaScript R-tree-based 2D spatial index for points and rectangles
At a glance
- What is it?
- RBush is a small MIT-licensed library that indexes 2D points and rectangles so bounding-box queries stop being linear scans. It is aimed at map and data-visualization code that holds tens of thousands of items in memory.
- Who is it for?
- Adopt RBush when your data is rectangles or dynamic points queried by bounding box in JavaScript, and when you can accept a rebuild-on-import model. Skip it for static point sets, where the README points at kdbush, and for nearest-neighbor queries, which live in rbush-knn.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 27 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The linear scan RBush replaces
A bounding-box query over a plain array means visiting every item. For a few hundred markers that is fine. For a map layer with tens of thousands of rectangles it is not, and the cost grows with the dataset rather than with the answer. RBush stores the same items in an R-tree, a tree whose internal nodes hold the bounding box of everything beneath them. A query walks only the branches whose boxes intersect the query box, so work scales with what matches plus the tree height. The README frames the gain bluntly: queries can run hundreds of times faster than looping over all items. The intended audience is narrow and clearly stated: maps and data visualizations, running in the browser or in Node.js. If your data is not 2D points or rectangles, this library has nothing to offer.
How the R-tree is built and searched
Items are objects with minX, minY, maxX and maxY. A point is a degenerate rectangle where min equals max. Insertion is non-recursive; when a node overflows, RBush splits it using the overlap-minimizing split routine from the R*-tree. The README is explicit that other R*-tree refinements, reinsertion on overflow and overlap-minimizing subtree search, were left out because they are too slow in JavaScript to pay for themselves. Deletion is depth-first and non-recursive, using a free-at-empty strategy: an underflowed node is not dissolved and its entries are not reinserted, it simply stays until it empties. That is a deliberate compromise between query and removal cost, and it means a tree that has seen heavy churn can hold partially empty nodes. Bulk loading uses the OMT algorithm (Overlap Minimizing Top-down Bulk Loading) with Floyd-Rivest selection; bulk insertion into a non-empty tree uses STLT (Small-Tree-Large-Tree), building a separate tree from the new items and inserting that smaller tree into the larger one. Search is a standard non-recursive R-tree traversal. The node size, passed to the constructor, defaults to 9; the README calls that reasonable for most applications and notes the trade-off directly: higher values mean faster insertion and slower search.
Installing RBush and running a first query
The package is published on npm as rbush and is ESM-first: package.json sets "type": "module" and "exports": "./index.js". The README gives the install command and the module import.
npm install rbushimport RBush from 'rbush';Create a tree, insert an item, and search a box. The search argument is always in {minX, minY, maxX, maxY} form regardless of how your data is shaped, and it returns the items whose boxes intersect that box.
const tree = new RBush();
const item = {
minX: 20,
minY: 40,
maxX: 30,
maxY: 50,
foo: 'bar'
};
tree.insert(item);
const result = tree.search({
minX: 40,
minY: 20,
maxX: 80,
maxY: 70
});For a batch, load() takes an array and is usually two to three times faster than inserting one by one.
tree.load([item1, item2, ...]);If your items are arrays rather than objects, subclass RBush and override toBBox, compareMinX and compareMinY, as the README demonstrates with an [x, y] point format. The browser path is a module import from jsDelivr, or a script tag exposing a global RBush.
Where the R-tree model bites back
Bulk insertion is the sharpest edge. Loading into an empty tree is the fast path. Loading into an existing tree builds a second tree from the new items and grafts it onto the first, which the README says works well when an update batch is clustered, and makes query performance worse when the new data is scattered. A steady trickle of geographically dispersed updates is the wrong workload for load(). Removal has its own trap: remove matches by reference by default, so the object you pass must be the object you inserted. Passing a copy requires a custom equals function, for example comparing an id field. The README also states two limits outright. For a static list of points that will not change after indexing, kdbush is the recommended tool and performs point indexing 5 to 8 times faster. And k-nearest-neighbor queries are not here at all; they live in a separate package, rbush-knn. Finally, export and import carry a constraint that is easy to miss: the nodeSize passed to the constructor must match in both trees or fromJSON will not reconstruct correctly. The README does not document a version compatibility guarantee for serialized trees, so a stored tree is only as portable as the nodeSize you wrote down.
RBush against a flat kdbush index
The nearest comparison is kdbush, from the same author and named in the RBush README. The difference is scope, not speed tuning. kdbush indexes points only, and only as a static set: the README recommends it when you do not need to add or remove points after indexing, and reports it as 5 to 8 times faster at point indexing. RBush handles rectangles as well as points and supports insert and remove after the fact, which is why it carries the R-tree machinery for splitting and underflow. Choosing between them is a question about your data's lifetime. A fixed set of coordinates rendered once, such as a scatter of locations on a chart, fits kdbush. A live layer where features are added and dropped as the user pans, filters or edits fits RBush. If you need nearest-neighbor lookups on top of RBush, that is rbush-knn, a separate install rather than a constructor option.
Maintenance, licence and upgrade cost
The repository is not archived, and the last push was on 2026-09-03. The most recent release is v4.0.1 from 2024-08-21, following v4.0.0 on 2024-06-27; the release before that, v3.0.1, dates to 2019-07-31. That spacing is worth reading as a signal: this is a small, settled library rather than one that ships features continuously, and the jump from 3.x to 4.x took roughly five years. The runtime dependency list is a single package, quickselect, and the published files are index.js plus two browser bundles, so the install surface is small. The licence is MIT, which permits commercial use and modification provided the copyright notice and permission notice are retained; the repository ships a LICENSE file. That is a description of the licence text, not legal advice, and anyone embedding RBush in a distributed product should read LICENSE directly. Upgrade cost is mostly the 3.x to 4.x boundary. If you are already on 4.0.1 there is nothing newer to move to. If you are on 3.x, the version number itself is the warning: this is a major bump, and the README documents no migration guide, so the test suite in test/ and the perf script in bench/ are the places to look before switching.
Editorial conclusion
Adopt RBush when your data is rectangles or dynamic points queried by bounding box in JavaScript, and when you can accept a rebuild-on-import model. Skip it for static point sets, where the README points at kdbush, and for nearest-neighbor queries, which live in rbush-knn. Before committing, verify that your nodeSize matches on both sides of any toJSON/fromJSON round trip and confirm how you will handle removals, since remove matches by reference unless you pass an equals function.
Frequently asked questions
How do I install RBush?
Install it from npm with npm install rbush, then import the default export with import RBush from 'rbush'. For the browser, the README shows a module import from jsDelivr or a script tag that exposes a global RBush.
What is spatial indexing in RBush?
RBush describes a spatial index as a data structure for points and rectangles that answers queries like all items within a bounding box very efficiently. It is most commonly used in maps and data visualizations.
When should I use kdbush instead of RBush?
The RBush README says that if you are indexing a static list of points and do not need to add or remove points after indexing, you should use kdbush, which performs point indexing 5 to 8 times faster than RBush.
How do I remove an item from an RBush tree?
Call tree.remove(item). By default RBush removes objects by reference, so you must pass the same object you inserted; to remove a copy, pass a custom equals function, such as one comparing an id field.
Does RBush support k-nearest-neighbor queries?
Not directly. The README points to a separate package, rbush-knn, for k nearest neighbors around a point queries.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/mourner-rbush)