A faster react-native-map-clustering — same API, same react-native-maps workflow, but clustering runs in C++ via Nitro instead of JavaScript on the RN bridge.
Built with Nitro Modules for high-performance native clustering.
Migration • Installation • Usage • Documentation • Example app
Full documentation is available at gmi-software.github.io/react-native-better-clustering. The Docusaurus source lives in docs/; run bun run docs:start to view it locally, or bun run docs:build to build static files.
- import MapView from 'react-native-map-clustering'
+ import MapView from 'react-native-better-clustering'
import { Marker } from 'react-native-maps'That is it. Your <Marker> children, cluster props, and react-native-maps options keep working — clustering just moves off the JS thread.
| react-native-map-clustering | This package | |
|---|---|---|
| API | MapView + <Marker> children |
Same |
| Map | react-native-maps |
react-native-maps |
| Engine | JS supercluster | C++ supercluster via Nitro |
| Scale | Good | 10k+ points without bridge jank |
- iOS
- Android
- Web
Note
Requires React Native 0.78+ with the New Architecture. Does not work in Expo Go — use a development build.
npm install react-native-better-clustering react-native-nitro-modules react-native-maps react-native-reanimated react-native-worklets
cd ios && pod install && cd ..Rebuild the native app after installing. Add the Worklets Babel plugin (last in the list) to babel.config.js:
plugins: ['react-native-worklets/plugin']npx expo install react-native-better-clustering react-native-nitro-modules react-native-maps react-native-reanimated react-native-workletsbabel-preset-expo wires up the Worklets plugin automatically.
Add the react-native-maps plugin to app.json:
{
"expo": {
"plugins": [
["react-native-maps", { "googleMapsApiKey": "YOUR_GOOGLE_MAPS_API_KEY" }]
]
}
}npx expo prebuild --clean
npx expo run:ios # or: npx expo run:androidimport MapView from 'react-native-better-clustering'
import { Marker } from 'react-native-maps'
export function MapScreen() {
return (
<MapView
style={{ flex: 1 }}
initialRegion={{
latitude: 52.23,
longitude: 21.01,
latitudeDelta: 0.35,
longitudeDelta: 0.35,
}}
radius={56}
minPoints={2}
clusterColor="#FFCC00"
clusterTextColor="#000000"
spiralEnabled
animationEnabled
onClusterPress={(cluster, markers) => {
console.log(cluster.properties.point_count, markers.length)
}}
>
{points.map((point) => (
<Marker
key={point.id}
coordinate={{
latitude: point.latitude,
longitude: point.longitude,
}}
/>
))}
{/* Keep a marker outside clusters */}
<Marker
cluster={false}
coordinate={{ latitude: 52.23, longitude: 21.01 }}
/>
</MapView>
)
}Pass renderCluster when you need full control over the cluster bubble:
import MapView, {
type RenderClusterProps,
} from 'react-native-better-clustering'
import { Marker } from 'react-native-maps'
const renderCluster = (cluster: RenderClusterProps) => {
const [longitude, latitude] = cluster.geometry.coordinates
return (
<Marker coordinate={{ latitude, longitude }} onPress={cluster.onPress}>
{/* your cluster bubble */}
</Marker>
)
}
<MapView renderCluster={renderCluster} {...props}>
{/* markers */}
</MapView>Everything from react-native-maps MapView, plus:
| Prop | Default | Description |
|---|---|---|
radius |
~6% of screen width |
Cluster radius in pixels |
minPoints |
2 |
Minimum points to form a cluster |
minZoom |
1 |
Minimum zoom level |
maxZoom |
20 |
Maximum zoom level for clustering |
clusteringEnabled |
true |
Toggle clustering |
spiralEnabled |
true |
Spider layout at max zoom |
spiderLineColor |
#FF0000 |
Color of spider connector lines |
clusterColor |
#0F52FF |
Default cluster bubble color (GMI brand blue) |
clusterTextColor |
#FFFFFF |
Default cluster label color |
animationEnabled |
true |
Animate cluster changes (iOS) |
onClusterPress |
— | (cluster, markers) => void |
renderCluster |
— | Custom cluster renderer |
preserveClusterPressBehavior |
false |
Skip auto fitToCoordinates |
superClusterRef |
— | Access the underlying engine |
width / height |
window size | Seed map dimensions before layout |
clusterUpdateIntervalMs |
100 |
Min interval (ms) between cluster updates while moving; 0 = only on settle |
Per-marker opt-out: <Marker cluster={false} ... />.
| Issue | Status |
|---|---|
Crash on NaN zoom (#294) |
Guarded with finite zoom fallback |
Map size can't be 0 on Android (#285) |
Layout-aware fitToCoordinates |
maxZoom / spiralEnabled ignored (#274, #275) |
Implemented |
| Unstable clusters on zoom (#255) | Stable cluster references between renders |
Per-marker cluster={false} (#297) |
Supported |
Caution
Bugs in react-native-maps itself (ghost markers on Android, navigation crashes) may still occur — this library replaces clustering, not the map renderer.
Today this library solves clustering compute — the C++ engine indexes 10k+ points and returns viewport clusters off the JS thread. Visible markers are still rendered through react-native-maps <Marker> components, which becomes the UI-thread bottleneck when hundreds of annotations are on screen at once.
The long-term goal is to remove that ceiling entirely.
Improvements within the current drop-in API: image-based cluster bubbles with caching, stable marker pooling, and smarter defaults. Expected to roughly 3–5× improve FPS at mid zoom levels, but still bounded by per-annotation overhead from react-native-maps.
A native clustered map view that draws annotations in a single native pass instead of mounting one React <Marker> per cluster or point. The existing C++ ClusterEngine feeds packed point data directly into native bubble rendering — same public MapView + <Marker> API, no N-marker mount storm on zoom.
| Approach | Typical visible markers @ ~60 FPS |
|---|---|
View-child <Marker> (today) |
~50–150 |
Image <Marker> + pooling (Tier 1) |
~200–400 |
| Native clustered map view (Tier 2) | 1000+ |
Tier 2 is the reason this library exists beyond a faster supercluster port: render as many markers as you need without FPS collapse. The compat API stays unchanged; rendering moves under the hood.
Track progress in GitHub Issues.
A minimal full-screen map with ~2,000 randomly scattered markers across Poland — default green cluster bubbles and standard map pins, modeled on the react-native-map-clustering demo.
cd example
bun install
npx expo prebuild --clean
bun run ios # or: bun run androidLow-level hooks and headless access live under subpath exports for custom map stacks:
| Import | Use when |
|---|---|
react-native-better-clustering/hooks |
useClusterer — you own the MapView |
react-native-better-clustering/clusterer |
Clusterer — declarative renderItem over useClusterer |
react-native-better-clustering/engine |
Supercluster, createClusterEngine, geometry helpers — see headless lifecycle |
react-native-better-clustering/geojson |
GeoJSON types and conversion |
react-native-better-clustering/utils |
packPoints, distance helpers |
See the docs for details. The /compat subpath is an alias of the main export.
| Problem | Solution |
|---|---|
| Map is blank on Android | Add a Google Maps API key. See platform setup. |
| New Architecture errors | Confirm New Architecture is enabled and rebuild. |
| Does not work in Expo Go | Use a development build. |
| Markers flicker on zoom | Memoize marker components; give each point a stable id. |
More in troubleshooting.
See CONTRIBUTING.md.
bun install
cd package && bun run typecheck && bun run lint && bun run testMIT — see LICENSE.

