Анализ производительности

Производительность в Leaflet определяется не только количеством объектов на карте, но и тем, как именно эти объекты создаются, рендерятся и обновляются в DOM. Архитектура библиотеки построена вокруг DOM-слоёв (SVG, Canvas, HTML-элементы), что делает её гибкой, но накладывает ограничения при работе с большими объёмами данных.

Ключевые узлы, влияющие на производительность:

  • количество DOM-элементов (маркер, полигон, попап)
  • частота перерисовки слоёв
  • стратегия загрузки тайлов
  • использование кластеризации и агрегации данных
  • тип рендерера (SVG или Canvas)

DOM как основное ограничение

Leaflet по умолчанию использует DOM-элементы для отображения объектов. Каждый маркер — это отдельный HTML-элемент, что при тысячах объектов приводит к резкому падению производительности.

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

  • 5 000+ маркеров на карте без оптимизации
  • частые обновления координат (анимация объектов)
  • одновременное использование popups и tooltips
  • сложные SVG-полигоны с большим числом вершин

DOM-операции являются дорогими: браузер вынужден пересчитывать layout, repaint и composite на каждом изменении.


Выбор рендерера: SVG и Canvas

Leaflet поддерживает два основных подхода к отрисовке векторных слоёв.

SVG-рендерер

SVG используется по умолчанию для векторных объектов.

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

  • каждый объект — отдельный DOM-узел
  • высокая точность взаимодействия (hover, click)
  • низкая производительность при большом количестве объектов

Подходит для:

  • небольших наборов геометрии
  • интерактивных карт с ограниченным числом слоёв

Canvas-рендерер

Canvas снижает нагрузку за счёт рисования в одном элементе.

Инициализация:

const map = L.map('map', {
  preferCanvas: true
});

или для отдельных слоёв:

const canvasRenderer = L.canvas();

const circle = L.circle([51.5, -0.09], {
  radius: 200,
  renderer: canvasRenderer
}).addTo(map);

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

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

Недостатки:

  • сложнее hit-testing
  • ограниченная интерактивность на уровне отдельных пикселей
  • менее точные события по сравнению с SVG

Маркеры и проблема масштабирования

Стандартные маркеры — один из самых дорогих элементов Leaflet.

Причины:

  • каждый маркер = DOM node
  • наличие иконки + shadow + события
  • перерасчёт позиции при каждом pan/zoom

Даже 2 000 маркеров могут вызывать заметные лаги при анимации карты.


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

Основной способ оптимизации — агрегация точек.

Популярная стратегия — использование Leaflet.markercluster.

const markers = L.markerClusterGroup();

for (let i = 0; i < 10000; i++) {
  const marker = L.marker([
    51.5 + Math.random(),
    -0.09 + Math.random()
  ]);

  markers.addLayer(marker);
}

map.addLayer(markers);

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

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

Эффект:

  • снижение нагрузки на рендеринг в десятки раз
  • улучшение взаимодействия при масштабировании

Оптимизация тайловых слоёв

L.tileLayer отвечает за загрузку растровых тайлов. Здесь производительность зависит от сети и кеширования.

Ключевые параметры:

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
  maxZoom: 19,
  updateWhenZooming: false,
  keepBuffer: 2,
  crossOrigin: true
}).addTo(map);

updateWhenZooming

Отключение обновления тайлов во время зума снижает нагрузку на сеть и CPU.

keepBuffer

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


Батчинг и группировка слоёв

Каждое добавление слоя в карту вызывает перерасчёт.

Проблема:

for (let i = 0; i < 1000; i++) {
  L.marker([lat, lng]).addTo(map);
}

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

const layerGroup = L.layerGroup();

for (let i = 0; i < 1000; i++) {
  layerGroup.addLayer(L.marker([lat, lng]));
}

layerGroup.addTo(map);

Разница:

  • одно обновление DOM вместо тысячи
  • снижение числа layout recalculation

Частота обновлений и throttling

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

Пример проблемы:

  • обновление координат каждые 16 мс (60 FPS)
  • перерасчёт позиций маркеров
  • перегрузка main thread

Решение — throttle:

function throttle(fn, limit) {
  let lastCall = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastCall >= limit) {
      lastCall = now;
      fn.apply(this, args);
    }
  };
}

const updateMarkers = throttle(() => {
  markers.forEach(m => m.setLatLng(getNewPosition(m)));
}, 100);

Эффект:

  • уменьшение количества перерисовок
  • стабильный FPS

Работа с геометрией и полигонами

Сложные полигоны с тысячами точек существенно нагружают браузер.

Проблемы:

  • вычисление геометрии при каждом zoom
  • дорогой path rendering в SVG
  • увеличение времени hit-testing

Оптимизации:

  • упрощение геометрии (simplification)
  • использование серверной подготовки данных
  • переключение на Canvas

Упрощение геометрии

Алгоритмы упрощения (например, Douglas–Peucker) уменьшают количество точек без значительной потери формы.

Пример подхода:

  • исходный полигон: 10 000 точек
  • упрощённый: 500–800 точек

Это снижает:

  • нагрузку на render pipeline
  • время обработки событий
  • стоимость перерисовки при zoom

Профилирование производительности

Основные инструменты:

  • Chrome DevTools Performance
  • FPS meter
  • Memory snapshot
  • Event listeners inspector

Метрики, которые важны:

  • scripting time
  • rendering time
  • painting time
  • number of DOM nodes
  • layer reflow frequency

Типичная проблема:

  • long tasks (>50 ms) при pan/zoom
  • layout thrashing при массовых updateLatLng

Оптимизация событий

Leaflet генерирует множество событий:

  • move
  • zoom
  • resize

Частые ошибки:

  • выполнение тяжёлой логики внутри move
  • синхронные API-запросы в обработчиках
  • перерисовка слоёв без необходимости

Оптимизация:

map.on('move', throttle(() => {
  updateVisibleObjects();
}, 50));

Виртуализация данных

При работе с большим количеством объектов важно отображать только то, что попадает в viewport.

Принцип:

  • получение текущих bounds
  • фильтрация объектов по координатам
  • отрисовка только видимых элементов
function getVisiblePoints(points, bounds) {
  return points.filter(p => bounds.contains([p.lat, p.lng]));
}

Это уменьшает:

  • количество маркеров
  • DOM-операции
  • нагрузку на взаимодействие

Память и утечки

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

Типичные причины:

  • не удалённые event listeners
  • сохранённые ссылки на map objects
  • повторное добавление слоёв без очистки

Правильное удаление:

map.removeLayer(layer);
layer.off();
layer = null;

Масштабирование архитектуры карты

При росте объёма данных важно переходить от прямого добавления объектов к структурированным подходам:

  • тайловая агрегация данных на сервере
  • векторные тайлы вместо GeoJSON
  • динамическая подгрузка по bounding box
  • уровни детализации (LOD)

Итоговые практики оптимизации

  • минимизация DOM-элементов
  • использование Canvas для больших наборов данных
  • кластеризация маркеров
  • ограничение частоты обновлений
  • упрощение геометрии
  • виртуализация viewport
  • батчинг операций добавления слоёв
  • контроль тайлового кеширования
  • профилирование в DevTools