При работе с интерактивными картами нередко возникает необходимость
отображать сотни, тысячи и даже десятки тысяч географических точек.
Каждый объект google.maps.Marker представляет собой
отдельный элемент, который участвует в процессе отрисовки, обработки
событий и управления состоянием карты. При увеличении количества
маркеров возникают следующие проблемы:
Например, создание нескольких тысяч объектов сразу после загрузки карты может привести к заметным задержкам даже на современных компьютерах.
const markers = [];
locations.forEach(location => {
const marker = new google.maps.Marker({
position: location,
map: map
});
markers.push(marker);
});
При небольшом количестве объектов такой подход работает хорошо, однако для крупных наборов данных требуется дополнительная оптимизация.
Самым эффективным способом оптимизации считается объединение близко расположенных маркеров в кластеры.
Кластер представляет собой специальный объект, отображающий количество точек внутри определённой области карты.
Вместо отображения тысячи отдельных маркеров карта показывает несколько кластеров:
[150]
[87]
[42]
При увеличении масштаба кластеры автоматически разбиваются на более мелкие группы, пока не появятся отдельные маркеры.
Современная библиотека кластеризации устанавливается через npm:
npm install @googlemaps/markerclusterer
Импорт:
import { MarkerClusterer } from "@googlemaps/markerclusterer";
Создание маркеров:
const markers = locations.map(location => {
return new google.maps.Marker({
position: location
});
});
Создание кластеризатора:
new MarkerClusterer({
map,
markers
});
После кластеризации производительность карты может улучшиться в несколько раз, поскольку браузеру требуется отображать значительно меньше объектов одновременно.
Не всегда необходимо создавать все маркеры сразу после загрузки страницы.
Если набор данных содержит тысячи записей, разумно создавать объекты постепенно.
Вместо единовременного создания:
locations.forEach(createMarker);
можно использовать пакетную обработку:
const batchSize = 100;
let index = 0;
function loadBatch() {
const batch = locations.slice(index, index + batchSize);
batch.forEach(createMarker);
index += batchSize;
if (index < locations.length) {
setTimeout(loadBatch, 0);
}
}
loadBatch();
Такой подход предотвращает длительную блокировку главного потока браузера.
Во многих проектах пользователю не требуется видеть все объекты одновременно.
Эффективным решением становится отображение только тех точек, которые попадают в текущую область просмотра карты.
const bounds = map.getBounds();
if (bounds.contains(marker.getPosition())) {
marker.setMap(map);
} else {
marker.setMap(null);
}
map.addListener("idle", updateVisibleMarkers);
Функция:
function updateVisibleMarkers() {
const bounds = map.getBounds();
markers.forEach(marker => {
if (bounds.contains(marker.getPosition())) {
marker.setMap(map);
} else {
marker.setMap(null);
}
});
}
Такой метод особенно полезен при работе с десятками тысяч точек.
Ещё более эффективным решением является получение данных с сервера только для текущей области карты.
const bounds = map.getBounds();
const northEast = bounds.getNorthEast();
const southWest = bounds.getSouthWest();
Формирование запроса:
const url =
`/api/points?` +
`north=${northEast.lat()}` +
`&east=${northEast.lng()}` +
`&south=${southWest.lat()}` +
`&west=${southWest.lng()}`;
Получение данных:
const response = await fetch(url);
const points = await response.json();
Преимущества подхода:
Во время перемещения карты событие drag срабатывает
постоянно.
map.addListener("drag", updateData);
Это приводит к огромному количеству операций.
Более правильный вариант:
map.addListener("idle", updateData);
Событие idle вызывается только после завершения
перемещения или масштабирования карты.
В результате количество обновлений сокращается в десятки и сотни раз.
Создание и удаление большого количества объектов вызывает дополнительную нагрузку на сборщик мусора JavaScript.
Вместо удаления объектов предпочтительно повторно использовать уже существующие экземпляры.
markers.forEach(marker => {
marker.setMap(null);
});
markers.length = 0;
Затем создаются новые объекты:
locations.forEach(location => {
markers.push(
new google.maps.Marker({
position: location,
map: map
})
);
});
marker.setPosition(newPosition);
marker.setMap(map);
Повторное использование объектов уменьшает количество выделений памяти и ускоряет работу приложения.
В современных версиях Google Maps рекомендуется использовать
AdvancedMarkerElement.
Подключение библиотеки:
const { AdvancedMarkerElement } =
await google.maps.importLibrary("marker");
Создание маркера:
const marker = new AdvancedMarkerElement({
map,
position: {
lat: 55.75,
lng: 37.61
}
});
Преимущества:
Для новых проектов этот вариант предпочтительнее классического
google.maps.Marker.
Каждый обработчик событий создаёт дополнительную нагрузку.
markers.forEach(marker => {
marker.addListener("click", () => {
showInfo(marker);
});
});
При наличии 10 000 маркеров создаётся 10 000 обработчиков.
Использование общей логики:
function markerClickHandler(marker) {
showInfo(marker);
}
markers.forEach(marker => {
marker.addListener(
"click",
markerClickHandler.bind(null, marker)
);
});
Ещё более эффективным решением является работа через кластеризацию и динамическую загрузку объектов, уменьшающую общее число активных маркеров.
Частой ошибкой является создание отдельного окна информации для каждого маркера.
const infoWindow = new google.maps.InfoWindow({
content: text
});
для каждого объекта.
При тысячах точек это приводит к лишнему потреблению памяти.
Создать один экземпляр:
const infoWindow = new google.maps.InfoWindow();
Использовать повторно:
marker.addListener("click", () => {
infoWindow.setContent(markerData.name);
infoWindow.open({
anchor: marker,
map
});
});
Такой подход является стандартной практикой оптимизации.
Чем сложнее внешний вид маркера, тем больше ресурсов требуется браузеру.
Наиболее затратными являются:
const icon = {
path: "M0,0 L10,0 L5,10 Z",
fillColor: "#ff0000",
fillOpacity: 1,
strokeWeight: 0,
scale: 1
};
Создание маркера:
new google.maps.Marker({
position,
icon,
map
});
Простые графические элементы масштабируются значительно лучше при массовом отображении объектов.
При быстром изменении масштаба и перемещении карты может выполняться множество одинаковых запросов.
Для ограничения частоты обновлений применяется дебаунсинг.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, delay);
};
}
Использование:
const updateMarkers =
debounce(loadData, 300);
map.addListener("idle", updateMarkers);
Преимущества:
Когда количество точек достигает десятков или сотен тысяч, даже кластеризация может оказаться недостаточной.
В таких случаях используются специализированные решения:
Пример подключения WebGL Overlay:
const overlay =
new google.maps.WebGLOverlayView();
overlay.setMap(map);
WebGL использует ресурсы видеокарты и способен отображать огромные объёмы геоданных значительно быстрее традиционных DOM-маркеров.
Для высоконагруженных карт обычно применяется комбинация нескольких методов:
idle.InfoWindow.AdvancedMarkerElement.Подобная архитектура позволяет поддерживать высокую скорость работы интерфейса даже при отображении сотен тысяч географических объектов на карте.