Оптимизация отображения множества маркеров

При работе с интерактивными картами нередко возникает необходимость отображать сотни, тысячи и даже десятки тысяч географических точек. Каждый объект google.maps.Marker представляет собой отдельный элемент, который участвует в процессе отрисовки, обработки событий и управления состоянием карты. При увеличении количества маркеров возникают следующие проблемы:

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

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

const markers = [];

locations.forEach(location => {
    const marker = new google.maps.Marker({
        position: location,
        map: map
    });

    markers.push(marker);
});

При небольшом количестве объектов такой подход работает хорошо, однако для крупных наборов данных требуется дополнительная оптимизация.


Использование кластеризации маркеров

Самым эффективным способом оптимизации считается объединение близко расположенных маркеров в кластеры.

Кластер представляет собой специальный объект, отображающий количество точек внутри определённой области карты.

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

Вместо отображения тысячи отдельных маркеров карта показывает несколько кластеров:

[150]
[87]
[42]

При увеличении масштаба кластеры автоматически разбиваются на более мелкие группы, пока не появятся отдельные маркеры.

Подключение MarkerClusterer

Современная библиотека кластеризации устанавливается через 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();

Преимущества подхода:

  • минимальный объём передаваемых данных;
  • снижение нагрузки на клиент;
  • уменьшение времени первоначальной загрузки;
  • возможность работы с миллионами записей.

Использование события idle вместо drag

Во время перемещения карты событие 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);

Повторное использование объектов уменьшает количество выделений памяти и ускоряет работу приложения.


Использование AdvancedMarkerElement

В современных версиях Google Maps рекомендуется использовать AdvancedMarkerElement.

Подключение библиотеки:

const { AdvancedMarkerElement } =
    await google.maps.importLibrary("marker");

Создание маркера:

const marker = new AdvancedMarkerElement({
    map,
    position: {
        lat: 55.75,
        lng: 37.61
    }
});

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

  • улучшенная производительность;
  • гибкая настройка внешнего вида;
  • использование DOM-элементов;
  • лучшая интеграция с современными API Google Maps.

Для новых проектов этот вариант предпочтительнее классического 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)
    );
});

Ещё более эффективным решением является работа через кластеризацию и динамическую загрузку объектов, уменьшающую общее число активных маркеров.


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

Частой ошибкой является создание отдельного окна информации для каждого маркера.

Неэффективный вариант

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
    });
});

Такой подход является стандартной практикой оптимизации.


Упрощение визуального оформления маркеров

Чем сложнее внешний вид маркера, тем больше ресурсов требуется браузеру.

Наиболее затратными являются:

  • сложные SVG;
  • анимации;
  • тени;
  • фильтры CSS;
  • большое количество вложенных DOM-элементов.

Лёгкий SVG

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);

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

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

Использование Canvas и WebGL для сверхбольших наборов данных

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

В таких случаях используются специализированные решения:

  • Canvas Overlay;
  • WebGL Overlay View;
  • Deck.gl;
  • собственные механизмы рендеринга поверх карты.

Пример подключения WebGL Overlay:

const overlay =
    new google.maps.WebGLOverlayView();

overlay.setMap(map);

WebGL использует ресурсы видеокарты и способен отображать огромные объёмы геоданных значительно быстрее традиционных DOM-маркеров.


Стратегия масштабируемого отображения маркеров

Для высоконагруженных карт обычно применяется комбинация нескольких методов:

  1. Загрузка данных по границам видимой области.
  2. Дебаунсинг запросов.
  3. Использование события idle.
  4. Кластеризация маркеров.
  5. Повторное использование объектов.
  6. Единый экземпляр InfoWindow.
  7. Упрощённые иконки.
  8. Переход на AdvancedMarkerElement.
  9. Использование Canvas или WebGL при экстремально больших объёмах данных.

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