При отображении большого количества точек на интерактивной карте возникает сразу несколько технических ограничений. Основная проблема связана с производительностью браузера: каждый маркер представляет собой DOM-элемент или SVG/Canvas-объект, и при их числе в несколько тысяч отрисовка, пересчёт позиций и обработка событий начинают существенно замедлять работу карты.
Дополнительный фактор — визуальная перегрузка. Даже если производительность остаётся приемлемой, плотное размещение маркеров делает карту нечитаемой: элементы перекрываются, теряется структура данных, усложняется восприятие пространственных закономерностей.
Кластеризация решает обе задачи одновременно:
В Leaflet стандартным решением является плагин Leaflet.markercluster, который реализует пространственное объединение маркеров с автоматическим перерасчётом при изменении масштаба и области просмотра.
Кластеризация в Leaflet строится вокруг структуры дерева, в которой точки группируются по радиусу в пикселях на текущем зуме.
При изменении масштаба происходит пересчёт:
В основе лежит принцип:
Плагин подключается отдельно от ядра Leaflet.
<link rel="stylesheet" href="MarkerCluster.css" />
<link rel="stylesheet" href="MarkerCluster.Default.css" />
<script src="leaflet.js"></script>
<script src="leaflet.markercluster.js"></script>
Создание кластерной группы:
const markers = L.markerClusterGroup();
Добавление маркеров:
markers.addLayer(L.marker([51.5, -0.09]));
markers.addLayer(L.marker([51.51, -0.1]));
markers.addLayer(L.marker([51.49, -0.08]));
map.addLayer(markers);
Внутренний алгоритм основан на последовательной агрегации точек:
Ключевой параметр — maxClusterRadius, определяющий плотность группировки:
const markers = L.markerClusterGroup({
maxClusterRadius: 80
});
Меньшие значения дают более детализированную карту, большие — более агрессивную агрегацию.
Каждое изменение zoom вызывает полную перестройку кластеров.
Процесс включает:
Leaflet.markercluster оптимизирует этот процесс за счёт:
Кластеры отображаются как специальные иконки, содержащие количество точек:
Пример кастомизации:
const markers = L.markerClusterGroup({
iconCreateFunction: function (cluster) {
const count = cluster.getChildCount();
return L.divIcon({
html: `<div class="cluster-icon">${count}</div>`,
className: 'custom-cluster',
iconSize: L.point(40, 40)
});
}
});
CSS:
.custom-cluster {
background: #2b8cbe;
border-radius: 50%;
color: white;
text-align: center;
line-height: 40px;
font-weight: bold;
}
При максимальном приближении кластеры могут распадаться на точки, которые находятся в одном пикселе. Для этого используется механизм spiderfy.
Он распределяет маркеры по окружности:
Настройка:
const markers = L.markerClusterGroup({
spiderfyOnMaxZoom: true
});
Кластеризация часто применяется к GeoJSON-данным.
const geoJsonLayer = L.geoJSON(data, {
pointToLayer: function (feature, latlng) {
return L.marker(latlng);
}
});
markers.addLayer(geoJsonLayer);
map.addLayer(markers);
При больших наборах данных (десятки тысяч объектов) добавление выполняется пакетно:
markers.addLayers(geoJsonPointsArray);
При работе с сервером часто используется стратегия подгрузки по границам карты.
Логика:
map.on('moveend', function () {
const bounds = map.getBounds();
fetch(`/api/points?bbox=${bounds.toBBoxString()}`)
.then(r => r.json())
.then(data => {
markers.clearLayers();
markers.addLayers(data.map(p => L.marker([p.lat, p.lng])));
});
});
При очень больших объёмах данных (миллионы точек) клиентская кластеризация становится недостаточной.
В таких случаях применяется серверная агрегация:
Типичный формат ответа:
{
"clusters": [
{ "lat": 51.5, "lng": -0.1, "count": 120 },
{ "lat": 51.49, "lng": -0.08, "count": 45 }
]
}
На клиенте такие данные отображаются как кастомные маркеры без внутренней логики группировки.
Ключевые факторы производительности:
Практические оптимизации:
L.markerClusterGroup({
animate: false
});
Основные параметры:
Пример конфигурации:
const markers = L.markerClusterGroup({
maxClusterRadius: 60,
disableClusteringAtZoom: 18,
spiderfyOnEveryZoom: true,
removeOutsideVisibleBounds: true
});
Плагин предоставляет события уровня кластера:
clusterclick — клик по группе;clustermouseover — наведение;animationend — завершение анимации.markers.on('clusterclick', function (a) {
console.log(a.layer.getAllChildMarkers().length);
});
Доступ к дочерним элементам:
const children = cluster.getAllChildMarkers();
В сложных системах используется комбинированный подход:
Такая схема снижает нагрузку и улучшает восприятие плотных данных.
В системах с высокой нагрузкой применяется разделение:
Дополнительно вводится кеширование кластеров по zoom-уровням, что позволяет избежать повторных вычислений при возврате к уже посещённым масштабам.