Работа с большим количеством объектов на карте в Google Maps JavaScript API быстро приводит к проблемам производительности, если каждый объект представлен отдельным DOM-маркером. При росте числа точек до нескольких тысяч и более основная нагрузка возникает не только при первичном рендеринге, но и при каждом изменении масштаба, перемещении карты и пересчёте проекций.
Виртуализация маркеров решает задачу отображения больших наборов геоданных за счёт динамического ограничения количества визуализируемых элементов в пределах текущего viewport и замены «полного набора объектов» на «подмножество, актуальное для кадра».
Классический google.maps.Marker (и его современный
аналог AdvancedMarkerElement) создаёт отдельный визуальный
элемент для каждой точки. Даже при умеренных объёмах данных проявляются
следующие ограничения:
При 5–10 тысячах маркеров браузер начинает упираться в ограничения рендеринга DOM, особенно на мобильных устройствах.
Виртуализация маркеров опирается на принцип:
рендерится только то, что видно в текущих границах карты + небольшой буфер вокруг viewport
Это означает:
bounds выполняется
фильтрация;Основой виртуализации является получение текущих границ:
const bounds = map.getBounds();
Каждая точка проверяется на принадлежность:
function isVisible(marker, bounds) {
return bounds.contains(new google.maps.LatLng(marker.lat, marker.lng));
}
Оптимизация заключается в том, чтобы:
LatLng для каждой проверки;Виртуализация всегда привязана к событиям:
idle — окончание перемещения/зумирования;bounds_changed — частые изменения (используется
осторожно);zoom_changed — смена уровня детализации;dragend — завершение перемещения.Наиболее стабильный вариант:
map.addListener('idle', updateMarkers);
Событие idle снижает количество пересчётов и
предотвращает перегрузку при анимации.
Базовый подход заключается в полном пересоздании маркеров при каждом обновлении viewport:
let markers = [];
function updateMarkers(data) {
const bounds = map.getBounds();
// удалить старые
markers.forEach(m => m.setMap(null));
markers = [];
// создать новые только для видимой области
data.forEach(point => {
if (bounds.contains(point.position)) {
const marker = new google.maps.Marker({
position: point.position,
map: map
});
markers.push(marker);
}
});
}
Недостаток такого подхода — постоянное уничтожение и создание объектов, что становится узким местом при большом количестве данных.
Более эффективная стратегия — пул объектов:
position и map.function updateMarkers(data) {
const bounds = map.getBounds();
let i = 0;
for (const point of data) {
if (bounds.contains(point.position)) {
if (!markers[i]) {
markers[i] = new google.maps.Marker();
}
markers[i].setPosition(point.position);
markers[i].setMap(map);
i++;
}
}
// скрыть лишние
for (let j = i; j < markers.length; j++) {
markers[j].setMap(null);
}
}
Такой подход снижает нагрузку на GC и DOM.
При больших наборах данных (100k+) линейная проверка становится неэффективной. Используются структуры:
На практике часто применяются библиотеки типа
supercluster, которые обеспечивают:
Кластеризация фактически является расширением виртуализации: вместо отдельных точек отображаются группы.
При увеличении масштаба кластеры «разворачиваются» в точки.
Используемая логика:
Классическая схема:
const index = new Supercluster({
radius: 60,
maxZoom: 16
});
index.load(features);
function update() {
const bounds = map.getBounds();
const zoom = map.getZoom();
const clusters = index.getClusters([
bounds.getSouthWest().lng(),
bounds.getSouthWest().lat(),
bounds.getNorthEast().lng(),
bounds.getNorthEast().lat()
], zoom);
}
LOD (Level of Detail) позволяет менять способ отображения в зависимости от масштаба:
Пример логики:
function getRenderMode(zoom) {
if (zoom < 6) return 'cluster';
if (zoom < 10) return 'simplified';
return 'full';
}
DOM-маркеры становятся узким местом при десятках тысяч объектов. Альтернативой является отрисовка через:
WebGLOverlayView).Преимущество:
Пример структуры overlay:
class CustomOverlay extends google.maps.OverlayView {
onAdd() {}
draw() {}
onRemove() {}
}
В draw() выполняется:
Ключевой элемент виртуализации — работа с проекцией:
const projection = overlay.getProjection();
const pixel = projection.fromLatLngToDivPixel(
new google.maps.LatLng(lat, lng)
);
Это позволяет:
Жёсткая привязка к границам карты вызывает «мигание» маркеров при небольших движениях. Поэтому добавляется буфер:
function extendBounds(bounds, factor = 0.2) {
const ne = bounds.getNorthEast();
const sw = bounds.getSouthWest();
const latDelta = (ne.lat() - sw.lat()) * factor;
const lngDelta = (ne.lng() - sw.lng()) * factor;
return new google.maps.LatLngBounds(
new google.maps.LatLng(sw.lat() - latDelta, sw.lng() - lngDelta),
new google.maps.LatLng(ne.lat() + latDelta, ne.lng() + lngDelta)
);
}
Проблема частых пересчётов решается через:
let pending = false;
map.addListener('bounds_changed', () => {
if (!pending) {
pending = true;
requestAnimationFrame(() => {
update();
pending = false;
});
}
});
Наиболее устойчивые системы комбинируют подходы:
Такая архитектура позволяет масштабировать отображение до сотен тысяч точек без деградации интерфейса.
| Подход | Производительность | Масштаб | Сложность |
|---|---|---|---|
| DOM-маркеры | низкая | до ~1k | низкая |
| виртуализация DOM | средняя | ~10k | средняя |
| кластеризация | высокая | ~100k | средняя |
| canvas overlay | высокая | ~500k+ | высокая |
| WebGL | очень высокая | 1M+ | очень высокая |
bounds_changed вместо
idle;Эффективная виртуализация строится вокруг: