Marker Clustering — Handling Thousands of Markers
Why Marker Clustering Matters
A camping map with hundreds or thousands of campsite markers becomes unusable — overlapping markers, slow rendering, and information overload. Marker clustering solves this by grouping nearby markers into clusters at lower zoom levels, and splitting them as the user zooms in.
Why this matters for your career:
- Marker clustering is essential for any map with more than ~50 markers
- Without clustering, map performance degrades rapidly
- Cluster algorithms are a classic computational geometry problem
- The technique transfers to any mapping application (real estate, logistics, tourism)
What Is Marker Clustering?
Marker clustering is a technique that groups nearby markers into a single cluster icon. The cluster shows the count of markers it contains. As the user zooms in, clusters split into smaller groups until individual markers are revealed.
How Clustering Works
Zoom Level 6: [42]
|
Zoom Level 8: [12] [30]
| |
Zoom Level 10: [4] [8] [15] [15]
| |
Zoom Level 12: Individual markers visible
Implementing MarkerCluster
Installation
npm install leaflet.markercluster
# or
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
Basic Usage
import L from 'leaflet';
import 'leaflet.markercluster';
// Create a marker cluster group
const mcg = L.markerClusterGroup({
maxClusterRadius: 50, // Cluster markers within 50px
spiderfyOnMaxZoom: true, // Show individual markers at max zoom
showCoverageOnHover: false, // Don't show cluster coverage area
zoomToBoundsOnClick: true // Zoom to cluster bounds on click
});
// Add markers to the cluster group
campsites.forEach(site => {
const marker = L.marker([site.latitude, site.longitude])
.bindPopup(`<b>${site.name}</b><br>${site.description}`);
mcg.addLayer(marker);
});
// Add the cluster group to the map
map.addLayer(mcg);
Configuration Options
| Option | Default | Description |
|--------|---------|-------------|
| maxClusterRadius | 80 | Maximum distance (px) to group markers |
| spiderfyOnMaxZoom | true | Show all markers when zoomed in |
| showCoverageOnHover | true | Highlight cluster coverage area |
| zoomToBoundsOnClick | true | Zoom to cluster bounds on click |
| disableClusteringAtZoom | null | Disable clustering at this zoom level |
| chunkedLoading | false | Add markers in chunks to avoid freezing |
| chunkInterval | 200 | Milliseconds between chunks |
| chunkDelay | 50 | Delay before starting chunked loading |
| singleMarkerMode | false | Treat single markers as clusters |
| removeOutsideVisibleBounds | true | Don't render markers outside viewport |
Custom Cluster Icons
const mcg = L.markerClusterGroup({
iconCreateFunction: function(cluster) {
const count = cluster.getChildCount();
let size = 'small';
let color = '#22c55e';
if (count >= 100) {
size = 'large';
color = '#ef4444';
} else if (count >= 20) {
size = 'medium';
color = '#f59e0b';
}
return L.divIcon({
html: `<div style="
background: ${color};
color: white;
width: ${size === 'large' ? '50px' : size === 'medium' ? '40px' : '30px'};
height: ${size === 'large' ? '50px' : size === 'medium' ? '40px' : '30px'};
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: ${size === 'large' ? '16px' : '14px'};
border: 3px solid white;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
">${count}</div>`,
className: '',
iconSize: size === 'large' ? [50, 50] : size === 'medium' ? [40, 40] : [30, 30]
});
}
});
Chunked Loading for Large Datasets
When adding thousands of markers, use chunked loading to keep the UI responsive:
const mcg = L.markerClusterGroup({
chunkedLoading: true,
chunkInterval: 100, // 100ms per chunk
chunkDelay: 500 // 500ms delay before starting
});
// Add 5000 markers without freezing the browser
const markers = [];
for (let i = 0; i < 5000; i++) {
const marker = L.marker([
23.5 + Math.random() * 3,
120.0 + Math.random() * 3
]);
markers.push(marker);
}
mcg.addLayers(markers); // chunkedLoading keeps UI responsive
map.addLayer(mcg);
Advanced Cluster Algorithms
For very large datasets (50,000+ markers), consider server-side clustering:
async function loadClusteredMarkers(bounds, zoom) {
const url = `/api/campsites/clustered?` +
`sw_lat=${bounds.getSouth()}&sw_lng=${bounds.getWest()}` +
`&ne_lat=${bounds.getNorth()}&ne_lng=${bounds.getEast()}` +
`&zoom=${zoom}`;
const response = await fetch(url);
const clusters = await response.json();
clusters.forEach(cluster => {
if (cluster.count === 1) {
// Single marker
const marker = L.marker([cluster.lat, cluster.lng]);
mcg.addLayer(marker);
} else {
// Cluster group
const clusterIcon = L.divIcon({
html: `<div class="cluster-icon">${cluster.count}</div>`,
iconSize: [40, 40],
className: ''
});
const clusterMarker = L.marker([cluster.lat, cluster.lng], {
icon: clusterIcon
});
mcg.addLayer(clusterMarker);
}
});
}
Performance Comparison
| Dataset Size | Without Clustering | With Clustering | |-------------|-------------------|-----------------| | 50 markers | Fast | Fast | | 500 markers | Noticeable lag | Fast | | 5000 markers | Browser freezes | Smooth | | 50,000 markers | Crash | Smooth (with chunked loading) | | 500,000 markers | Crash | Smooth (with server-side clustering) |
Best Practices
| Practice | Reason |
|----------|--------|
| Use maxClusterRadius: 50-80 | Good balance between grouping and detail |
| Enable chunkedLoading for large datasets | Keeps UI responsive during marker addition |
| Customize cluster icon by count | Visual cues for density (green/yellow/red) |
| Use spiderfyOnMaxZoom: true | See all individual markers at max zoom |
| Use removeOutsideVisibleBounds: true | Don't render markers outside the viewport |
| Consider server-side clustering for 50k+ | Send only clustered data from the server |
| Use popups instead of tooltips for details | Popups work better on mobile touch |
| Test on mobile devices | Clustering that works on desktop may lag on mobile |
Summary
Marker clustering is essential for any map application with more than a few dozen markers. Leaflet.markercluster provides a fast, configurable clustering solution. For very large datasets, use chunked loading or server-side clustering. Custom cluster icons provide visual density feedback.
Key takeaways:
L.markerClusterGroup()creates a cluster groupmaxClusterRadiuscontrols how close markers must be to clusterchunkedLoadingprevents browser freezing with large datasets- Custom cluster icons change color/size based on count
spiderfyOnMaxZoomreveals individual markers at highest zoom- Without clustering: 5000+ markers causes freezing
- With clustering: 50,000+ markers is smooth
- For 100k+ markers, use server-side clustering
What's Next: Geolocation
The next chapter covers geolocation — getting user location, centering the map, calculating distances, and handling permissions.