Your map is lagging. You added dozens, then hundreds of markers, and now panning feels sluggish, zooming stutters, and your users are frustrated. This is the DOM overload problem, and the solution is marker clustering. Clustering groups nearby points into a single icon, dramatically improving performance and transforming a chaotic mess into a clear, interactive overview. This guide provides the working code and clear comparisons you need to implement clustering in the three major JavaScript mapping libraries: Leaflet, Mapbox GL JS (and its open-source sibling MapLibre GL JS), and OpenLayers.
Why Your Map Slows Down (And How Clustering Fixes It)
Every marker you add to a map is a complex DOM element. It’s not just an image. It’s an icon with a position, potential popup HTML, click and hover event listeners, and a shadow DOM tree. Browsers are efficient, but they have limits. Rendering 500 of these elements is manageable. Rendering 5,000 can bring even powerful machines to a crawl, as the browser’s main thread gets bogged down in style calculations, layout, and paint operations for each item.
The DOM Overload Problem
Think of each marker as an open browser tab. A few are fine, but open a hundred and your computer starts to struggle. Similarly, each marker consumes memory and CPU cycles. When a user pans or zooms, the browser must recalculate the position and visibility of every single marker, leading to janky, unresponsive interactions. This problem scales poorly, making client-side rendering of large point datasets impractical.
Clustering: From Chaos to Clarity
Clustering solves this by aggregating nearby points based on the current map view. When zoomed out, dozens of individual markers collapse into one cluster icon, often labeled with the count. As the user zooms in, clusters break apart to reveal smaller clusters and, eventually, the individual points. This serves a dual purpose. First, it provides a massive performance boost by reducing the number of active DOM elements or rendered features by an order of magnitude. Second, it dramatically improves the user experience. A map showing 10,000 points as 10,000 tiny icons is useless. Showing it as 200 clusters gives an immediate, comprehensible overview of data density and distribution.
Leaflet Clustering: The Simple & Powerful Plugin Approach
For most projects using Leaflet, the solution is the excellent Leaflet.markercluster plugin. It’s the de facto standard, turning Leaflet’s simplicity into a powerful tool for handling thousands of points.
Setting Up Leaflet.markercluster
Start by including the plugin’s CSS and JS, available via CDN. Then, instead of adding markers directly to the map, you add them to a MarkerClusterGroup.
<!-- Include Leaflet and the MarkerCluster plugin -->
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.Default.css" />
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet.markercluster/dist/leaflet.markercluster.js"></script>
<div id="map" style="height: 500px;"></div>
<script>
// Initialize the map
const map = L.map('map').setView([51.505, -0.09], III);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
// Create a marker cluster group
const markers = L.markerClusterGroup();
// Generate or load some sample data (e.g., from GeoJSON)
const samplePoints = [
{ lat: 51.5, lng: -0.09, name: "Point A" },
{ lat: 51.51, lng: -0.1, name: "Point B" },
// ... hundreds more points
];
// Add markers to the cluster group
samplePoints.forEach(point => {
const marker = L.marker([point.lat, point.lng])
.bindPopup(`<b>${point.name}</b>`);
markers.addLayer(marker);
});
// Add the cluster group to the map
map.addLayer(markers);
</script>
With just these lines, you have a fully functional clustered map. The plugin handles the complex logic of grouping, spiderfying (spreading out overlapping markers on click), and performance optimization.
Customizing Cluster Icons and Behavior
The plugin is highly customizable. You can change how clusters look based on the number of points they contain and define what happens when users interact with them.
const markers = L.markerClusterGroup({
spiderfyOnMaxZoom: true,
showCoverageOnHover: false,
iconCreateFunction: function (cluster) {
const count = cluster.getChildCount();
let size = 'small';
if (count > 100) size = 'large';
else if (count > 10) size = 'medium';
// Return a new L.DivIcon with custom HTML/CSS based on size and count
return L.divIcon({
html: `<div class="cluster-${size}"><span>${count}</span></div>`,
className: 'custom-cluster-icon',
iconSize: L.point(40, 40)
});
}
});
This ecosystem of plugins is a key reason Leaflet remains a top choice for simple to medium-complexity interactive maps.
Mapbox GL JS & MapLibre GL JS: Built-in, GPU-Powered Clustering
Mapbox GL JS and its open-source fork, MapLibre GL JS, take a different, integrated approach. Clustering is a native feature of the vector tile pipeline, leveraging WebGL for GPU-accelerated rendering. This method is powerful and flexible, controlled through source and layer configuration.
Configuring a Clustered GeoJSON Source
Instead of managing marker objects, you define a GeoJSON source with clustering enabled. Then you create separate layers for the clusters and the individual points.
// Map initialization (works for both Mapbox and MapLibre, just change the style URL)
mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN'; // Omit for MapLibre
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/light-vII', // Use 'https://demotiles.maplibre.org/style.json' for MapLibre
center: [-0.09, 51.505],
zoom: III
});
map.on('load', () => {
// Add a clustered GeoJSON source
map.addSource('earthquakes', {
type: 'geojson',
data: 'https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson', // Your GeoJSON URL
cluster: true,
clusterMaxZoom: 14, // Max zoom to cluster points on
clusterRadius: 50 // Radius of each cluster in pixels
});
// Add a layer for the clusters (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'],
20,
100,
30,
750,
40
]
}
});
// Add a layer for the individual points (unclustered)
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'earthquakes',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#IIb2d8',
'circle-radius': 4,
'circle-stroke-width': I,
'circle-stroke-color': '#fff'
}
});
});
Styling Clusters and Handling Clicks
The real power lies in the expression-based styling system. You can also add interactivity, like clicking a cluster to zoom to its bounds.
// Add cluster count labels
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
}
});
// Click on a cluster to zoom in
map.on('click', 'clusters', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['clusters']
});
const clusterId = features[0].properties.cluster_id;
const source = map.getSource('earthquakes');
source.getClusterExpansionZoom(clusterId, (err, zoom) => {
if (err) return;
map.easeTo({
center: features[0].geometry.coordinates,
zoom: zoom
});
});
});
For a deeper dive into building with this library, our Mapbox GL JS tutorial covers markers, popups, and GeoJSON in detail. Remember, while Mapbox offers convenience and advanced services, MapLibre provides the same core rendering engine as a fully open-source alternative, a crucial distinction for cost-sensitive or vendor-agnostic projects.
OpenLayers Clustering: The GIS Powerhouse Method
OpenLayers, favored for complex GIS applications, handles clustering via its ol/source/Cluster. This source wraps a standard vector source, grouping features based on pixel distance. It’s a robust, programmatic approach suited for advanced scenarios.
Using the ol/source/Cluster Source
The setup involves creating a clustered source and a vector layer with a style function that dynamically adjusts based on cluster size.
import { Map, View } from 'ol';
import { OSM, Vector as VectorSource } from 'ol/source';
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer';
import { Cluster } from 'ol/source';
import { Style, Circle, Fill, Stroke, Text } from 'ol/style';
import { fromLonLat } from 'ol/proj';
// Create a vector source with some random points
const vectorSource = new VectorSource();
for (let i = 0; i < 1000; i++) {
const feature = new ol.Feature({
geometry: new ol.geom.Point(fromLonLat([
Math.random() * 10 - 5, // Longitude
Math.random() * 8 - 4 // Latitude
]))
});
vectorSource.addFeature(feature);
}
// Wrap the source with a Cluster source
const clusterSource = new Cluster({
distance: 40, // Pixel distance for clustering
source: vectorSource
});
// Create a style function for the clusters
const clusterStyle = (feature) => {
const size = feature.get('features').length;
const color = size > 100 ? '#f28cb1' : size > 10 ? '#f1f075' : '#51bbd6';
const radius = Math.max(8, Math.min(20, Math.sqrt(size) * 2));
return new Style({
image: new Circle({
radius: radius,
fill: new Fill({ color: color }),
stroke: new Stroke({
color: '#fff',
width: 2
})
}),
text: new Text({
text: size.toString(),
fill: new Fill({ color: '#fff' }),
font: 'bold 12px sans-serif'
})
});
};
// Create the map with the clustered layer
const map = new Map({
target: 'map',
layers: [
new TileLayer({
source: new OSM()
}),
new VectorLayer({
source: clusterSource,
style: clusterStyle
})
],
view: new View({
center: fromLonLat([0, 50]),
zoom: 4
})
});
This approach gives you fine-grained control and integrates seamlessly with OpenLayers’ extensive GIS toolset, though it comes with a steeper initial learning curve compared to Leaflet.
Leaflet vs. Mapbox/MapLibre vs. OpenLayers: Which One Should You Choose?
The right choice depends on your project’s specific needs. Here’s a clear comparison to guide your decision.
| Criteria | Leaflet + MarkerCluster | Mapbox GL JS / MapLibre GL JS | OpenLayers |
|---|---|---|---|
| Ease of Setup | Very easy. Plugin-based, minimal configuration. | Moderate. Requires understanding of sources/layers and style expressions. | Complex. Steeper learning curve, more boilerplate code. |
| Performance Ceiling (Client-Side) | Good for up to ~50k points. DOM-based limits become apparent with huge datasets. | Very High. WebGL rendering handles tens of thousands of points smoothly. | High. Efficient Canvas/WebGL rendering, suitable for complex GIS data. |
| Customization | High for markers/clusters via plugin options and CSS. | Extremely High. Full programmatic control over every visual property with expressions. | Very High. Complete programmatic control via style functions and APIs. |
| Cost | Free (OpenStreetMap tiles) or nominal (paid tile providers). | Mapbox: Paid tiers after low free limits. MapLibre: Free and open-source. | Free and open-source. |
| Best For | Simple to medium-complexity maps, quick prototypes, projects where ease-of-use is paramount. | Modern, highly-styled interactive maps, data visualization, applications needing smooth performance with large datasets. | GIS-heavy applications, scientific visualizations, projects requiring advanced coordinate systems and data formats. |
Performance and Scale: When Client-Side Clustering Isn’t Enough
Client-side clustering has a limit. If you’re dealing with truly massive datasets (100,000+ points), processing and rendering everything in the browser will eventually fail, regardless of the library. The data transfer alone can crash the page. When you hit this wall, you need to move the work to the server.
Two main alternatives exist:
- Server-Side Clustering/Aggregation: Your backend pre-clusters the data into fewer, aggregated points (or counts per region) before sending it to the frontend. This sends far less data over the wire. Tools like PostGIS (with
ST_ClusterWithin) or dedicated geospatial databases excel at this. - Vector Tiles: This is the industrial-grade solution. Your data is sliced into pyramid-like tiles at different zoom levels, and only the tiles needed for the current viewport are requested and rendered. Both Mapbox/MapLibre and OpenLayers have excellent vector tile support. This requires a significant upfront investment in tile generation infrastructure (using tools like Tippecanoe, GeoServer, or MapTiler).
The trade-off is clear: client-side clustering is simple and fast to implement but scales to a point. Server-side solutions scale infinitely but add significant backend complexity and development time.
Pro Tips for a Smooth User Experience
Beyond basic implementation, these tips will polish your clustered map:
- Tune the Cluster Radius: The default pixel distance (e.g., 50px in Mapbox, 40px in OpenLayers) works for most maps. Increase it for sparser data to form fewer, larger clusters. Decrease it for very dense data to avoid premature over-aggregation.
- Design Clear Cluster Icons: The cluster icon must communicate its contents. Always show the count. Use color and size gradients (small/light for few points, large/vibrant for many) to intuitively convey density.
- Manage Popups Thoughtfully: Clicking a single marker should show its popup. Clicking a cluster should typically zoom in or spiderfy. Avoid showing a popup listing hundreds of items inside a cluster.
- Optimize Your GeoJSON: Before clustering, simplify your GeoJSON geometry if possible and strip unnecessary properties to reduce payload size.
- Test on Low-End Devices: Always check performance on a mid-range mobile phone or older laptop. This is where performance gains from clustering are most crucial.
Clustering transforms a crippling performance problem into a feature that enhances both speed and usability. By implementing it with Leaflet’s straightforward plugin, Mapbox/MapLibre’s powerful native system, or OpenLayers’ flexible GIS approach, you ensure your map remains fast, clear, and engaging, no matter how much data you throw at it.