Отрисовка большого количества маркеров

При увеличении числа объектов на карте Leaflet основное узкое место возникает не в загрузке данных, а в их отрисовке и управлении DOM-элементами. Каждый маркер (L.marker) в стандартной реализации представляет собой отдельный HTML-элемент, привязанный к слою карты. Это означает, что при добавлении тысяч точек браузер вынужден поддерживать тысячи DOM-узлов, обрабатывать их позиционирование и события.

Ключевые ограничения стандартного подхода:

  • линейное увеличение нагрузки на DOM при росте числа маркеров
  • высокая стоимость перерасчёта позиций при панорамировании и зуме
  • значительное потребление памяти
  • деградация FPS при взаимодействии с картой

Даже при сравнительно мощном устройстве порог в 3–5 тысяч маркеров может приводить к заметным лагам без оптимизации.


Базовая модель работы маркеров Leaflet

Каждый маркер в Leaflet проходит несколько этапов:

  1. Создание объекта L.marker(latlng)
  2. Привязка к карте через .addTo(map)
  3. Создание DOM-элемента (иконки)
  4. Привязка событий (click, hover и др.)
  5. Пересчёт позиции при каждом изменении viewport
const marker = L.marker([51.505, -0.09]).addTo(map);

При масштабировании системы до тысяч объектов основной расход ресурсов приходится на этапы 3–5.


Критическая особенность: DOM как ограничение

Leaflet по умолчанию использует HTML-рендеринг маркеров. Это удобно, но плохо масштабируется.

Проблема заключается в том, что:

  • каждый маркер = отдельный DOM-узел
  • каждый узел участвует в layout и paint циклах браузера
  • события обрабатываются индивидуально

В результате возникает квадратичное ухудшение производительности при увеличении плотности объектов.


Отключение интерактивности маркеров

Первый уровень оптимизации — отказ от ненужных интерактивных возможностей.

L.marker([51.5, -0.09], {
  interactive: false
}).addTo(map);

Отключение интерактивности снижает количество event listener-ов и уменьшает нагрузку на систему событий Leaflet.

Также полезно отключать:

L.marker([51.5, -0.09], {
  keyboard: false,
  title: ''
});

Использование pane для оптимизации слоёв

Leaflet позволяет управлять слоями через panes, что влияет на порядок отрисовки и частично на производительность.

map.createPane('markersPane');
map.getPane('markersPane').style.zIndex = 650;

const marker = L.marker([51.5, -0.09], {
  pane: 'markersPane'
}).addTo(map);

Разделение маркеров по слоям позволяет:

  • уменьшить пересечения перерисовки
  • логически группировать объекты
  • управлять приоритетом отображения

Кластеризация маркеров

Основной метод масштабирования — объединение маркеров в кластеры.

Используется библиотека:

  • leaflet.markercluster

Принцип работы:

  • близкие маркеры объединяются в один визуальный объект
  • при увеличении масштаба кластеры раскрываются
const markers = L.markerClusterGroup();

markers.addLayer(L.marker([51.5, -0.09]));
markers.addLayer(L.marker([51.51, -0.1]));

map.addLayer(markers);

Преимущества:

  • резкое снижение количества DOM-элементов
  • стабильная производительность при десятках тысяч точек
  • адаптивное поведение при зуме

Виртуализация маркеров

Альтернативный подход — рендеринг только видимой области.

Принцип:

  • хранится полный набор данных
  • отображаются только точки в текущем bounding box
function renderVisibleMarkers() {
  const bounds = map.getBounds();

  markersData.forEach(point => {
    if (bounds.contains(point.latlng)) {
      L.marker(point.latlng).addTo(map);
    }
  });
}

Обновление при перемещении карты:

map.on('moveend', renderVisibleMarkers);

Преимущество:

  • минимальное количество активных DOM-элементов

Недостаток:

  • частые пересоздания маркеров при движении карты

Canvas-рендеринг вместо DOM

Leaflet поддерживает Canvas как альтернативу HTML-слою через L.canvas().

const canvasRenderer = L.canvas();

L.circleMarker([51.5, -0.09], {
  renderer: canvasRenderer
}).addTo(map);

Особенности:

  • один canvas вместо множества DOM-узлов
  • отрисовка через GPU/bitmap pipeline
  • значительно выше производительность при больших объёмах

Однако:

  • ограниченная интерактивность
  • сложнее кастомизация внешнего вида
  • события обрабатываются иначе

Использование circleMarker вместо marker

L.circleMarker менее затратен, чем L.marker, поскольку не создаёт HTML-иконку.

L.circleMarker([51.5, -0.09], {
  radius: 5
}).addTo(map);

Причины высокой производительности:

  • геометрия рисуется как вектор
  • отсутствует HTML DOM-узел
  • лучше масштабируется при больших данных

Динамическое добавление маркеров

Добавление тысяч маркеров за один цикл блокирует основной поток.

Проблема:

for (let i = 0; i < 10000; i++) {
  L.marker(getRandomLatLng()).addTo(map);
}

Оптимизация через батчинг:

let i = 0;

function addBatch() {
  const batchSize = 200;

  for (let j = 0; j < batchSize && i < data.length; j++, i++) {
    L.marker(data[i].latlng).addTo(map);
  }

  if (i < data.length) {
    requestAnimationFrame(addBatch);
  }
}

addBatch();

Преимущество:

  • отсутствие блокировки UI
  • равномерная нагрузка на кадры браузера

Использование LayerGroup для управления множеством объектов

LayerGroup позволяет группировать маркеры и управлять ими как единым объектом.

const group = L.layerGroup();

data.forEach(item => {
  group.addLayer(L.marker(item.latlng));
});

map.addLayer(group);

Преимущества:

  • массовое добавление/удаление
  • централизованное управление видимостью
  • упрощение логики

Минимизация тяжёлых иконок

Кастомные иконки часто становятся причиной падения производительности.

Проблемные случаи:

  • большие PNG/SVG файлы
  • тени и сложные фильтры
  • анимации CSS

Оптимизированный вариант:

const icon = L.icon({
  iconUrl: 'marker.png',
  iconSize: [16, 16],
  iconAnchor: [8, 8]
});

Рекомендации по снижению нагрузки:

  • уменьшение разрешения иконок
  • отказ от теней
  • использование простых форм

Управление частотой обновления карты

При интенсивных обновлениях (например, поток GPS-данных) необходимо ограничивать частоту перерисовки.

let lastUpdate = 0;

function updateMarkers() {
  const now = Date.now();

  if (now - lastUpdate < 100) return;

  lastUpdate = now;

  // обновление маркеров
}

Это предотвращает перегрузку render loop.


Сравнение подходов отрисовки большого количества маркеров

  • DOM-маркеры

    • простота
    • низкая масштабируемость
  • ClusterGroup

    • высокая производительность
    • зависимость от стороннего плагина
  • Canvas renderer

    • максимальная производительность
    • ограниченная интерактивность
  • CircleMarker

    • баланс между скоростью и гибкостью
  • Виртуализация

    • контроль над данными
    • повышенная сложность реализации

Комбинированные стратегии оптимизации

На практике используется сочетание нескольких методов:

  • кластеризация для общего уровня zoom
  • canvas или circleMarker для плотных областей
  • виртуализация для огромных наборов данных
  • батчинг при загрузке
  • отключение интерактивности для второстепенных объектов

Такой подход позволяет поддерживать стабильную работу карты даже при десятках и сотнях тысяч точек без существенной деградации производительности.