Using Mapbox GL JS client-side clustering for large GeoJSON point sets
Learn how to enable client-side clustering in Mapbox GL JS to render thousands of points efficiently, with a code example and performance considerations.
05 Jun 2026, 20:13 UTC

When you try to render tens of thousands of individual point features on a Mapbox GL JS map, the browser can become sluggish and the user experience suffers. Enabling built‑in clustering lets the library group nearby points on the fly, reducing the number of shapes drawn while still letting users drill down to the original data.
How clustering works in Mapbox GL JS
You declare a GeoJSON source with cluster: true. The library then creates clusters at render time based on a pixel radius (clusterRadius) and a maximum zoom level (clusterMaxZoom). As the user pans or zooms, clusters are recomputed without refreshing the source. You can compute aggregate properties (e.g., sum, average) with clusterProperties using Mapbox expressions.
Worked example
The following snippet shows a minimal setup for a point dataset that displays cluster circles, a label with the point count, and the original points at higher zoom levels.
// Assuming mapboxgl is already loaded and a map instance exists
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/light-v11',
center: [-96, 37.8],
zoom: 3
});
map.on('load', () => {
// Add a GeoJSON source with clustering enabled
map.addSource('earthquakes', {
type: 'geojson',
data: 'https://example.com/data/earthquakes.geojson',
cluster: true,
clusterRadius: 50, // radius in pixels
clusterMaxZoom: 14, // zoom to start clustering
clusterProperties: {
// sum of magnitudes for all points in the cluster
'magnitude_sum': ['+', ['get', 'mag']],
// count of points (default)
'point_count': ['+', 1]
}
});
// Layer for clustered circles
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'earthquakes',
filter: ['has', 'point_count'],
paint: {
'circle-color': [
'step',
['get', 'point_count'],
'#51bbd6',
100, '#f1f075',
750, '#f28cb1'
],
'circle-radius': [
'step',
['get', 'point_count'],
15,
100, 20,
750, 25
]
}
});
// Layer for cluster labels (count)
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'earthquakes',
filter: ['has', 'point_count'],
layout: {
'text-field': ['get', 'point_count_abbreviated'],
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 12
}
});
// Layer for unclustered points (only visible at higher zoom)
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'earthquakes',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 4,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff'
}
});
// Click handler to zoom into a cluster
map.on('click', 'clusters', (e) => {
const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
const clusterId = features[0].properties.cluster_id;
map.getSource('earthquakes').getClusterExpansionZoom(
clusterId,
(err, zoom) => {
if (err) return;
map.easeTo({
center: features[0].geometry.coordinates,
zoom: zoom
});
}
);
});
});
Trade‑offs and limitations
- Clustering runs on the main thread while the source loads; very large GeoJSON files (hundreds of thousands of points) can cause noticeable delays before the map becomes interactive.
- Because clustering works on the data already loaded into the source, it does not reduce network payload size. If you need to load massive datasets, consider tiling the data into vector tiles or using a library like Supercluster in a worker.
clusterPropertiesexpressions are evaluated once when the source loads; they cannot react to runtime changes without recreating the source.
Actionable closing
Start with the simple source configuration shown above and test with a dataset that matches your expected production size. Measure the time‑to‑interactive; if the delay is acceptable, client‑side clustering is a convenient way to keep your map responsive. If load times become problematic, move to a vector‑tile strategy or pre‑aggregate points server‑side.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.