Минимизация перерисовок

Перерисовки карты в Google Maps JavaScript API становятся основным источником деградации производительности при работе с большим количеством объектов, динамическими обновлениями состояния и частыми изменениями пользовательского интерфейса. Минимизация перерисовок сводится к контролю жизненного цикла карты, снижению числа операций, влияющих на DOM и WebGL-контекст, а также к переиспользованию уже созданных сущностей вместо их пересоздания.

Каждое изменение состояния карты потенциально вызывает перерасчёт:

  • матрицы отображения тайлов
  • позиций overlay-слоёв
  • трансформаций маркеров и векторных объектов
  • пересчёта видимой области (viewport)

Наиболее дорогими операциями являются:

  • создание нового экземпляра карты
  • повторное добавление маркеров
  • полная замена набора overlay-объектов
  • частые вызовы setOptions с множеством параметров

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

Переиспользование экземпляра карты

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

const map = new google.maps.Map(document.getElementById("map"), {
  center: { lat: 50.45, lng: 30.52 },
  zoom: 10,
});

Дальнейшие изменения выполняются через методы:

map.setCenter({ lat: 50.46, lng: 30.60 });
map.setZoom(12);

Избыточная практика — пересоздание карты при каждом изменении данных — приводит к полной перерисовке всех слоёв и сбросу состояния.

Контроль частоты обновлений состояния

Частые события (scroll, mousemove, input) могут вызывать обновления карты десятки раз в секунду. Без ограничения частоты вызовов возникает эффект постоянной перерисовки.

Используются техники:

  • debounce — задержка выполнения
  • throttle — ограничение частоты
  • requestAnimationFrame — синхронизация с рендером браузера
function throttle(fn, delay) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= delay) {
      last = now;
      fn(...args);
    }
  };
}

window.addEventListener("mousemove",
  throttle((e) => {
    map.setCenter({ lat: e.clientY * 0.001, lng: e.clientX * 0.001 });
  }, 50)
);

Главный эффект — уменьшение количества трансформаций viewport и пересчётов координат.

Минимизация операций с маркерами

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

Антипаттерн:

markers.forEach(m => m.setMap(null));
markers = [];
markers.forEach(data => {
  const marker = new google.maps.Marker({
    position: data.position,
    map
  });
  markers.push(marker);
});

Каждый setMap(null) и повторное создание маркера инициирует перерасчёт слоя.

Оптимальный подход — обновление существующих экземпляров:

markers.forEach((marker, i) => {
  marker.setPosition(data[i].position);
});

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

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

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

Кластеризация объединяет маркеры в группы, уменьшая количество DOM/overlay элементов.

const clusterer = new markerClusterer.MarkerClusterer({
  map,
  markers
});

С точки зрения перерисовок это снижает:

  • количество отдельных overlay-элементов
  • число обработчиков событий
  • стоимость hit-testing при взаимодействии

Оптимизация обновления viewport

Частые вызовы:

  • setCenter
  • panTo
  • fitBounds

могут приводить к каскадным перерисовкам.

Особенно дорог fitBounds, так как он:

  • вычисляет оптимальный zoom
  • пересчитывает тайловую сетку
  • триггерит обновление всех overlay

Оптимизация заключается в группировке изменений:

map.setOptions({
  center: newCenter,
  zoom: newZoom
});

Единичный вызов снижает количество внутренних перерасчётов.

Ограничение использования setOptions

setOptions пересоздаёт внутренние конфигурации карты и может вызывать лишние обновления.

Антипаттерн:

map.setOptions({ zoom: map.getZoom() + 1 });
map.setOptions({ disableDefaultUI: true });
map.setOptions({ draggable: false });

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

Правильный подход — группировка изменений:

map.setOptions({
  zoom: map.getZoom() + 1,
  disableDefaultUI: true,
  draggable: false
});

Переиспользование overlay и кастомных слоёв

Кастомные overlay (например, OverlayView) часто создаются заново при обновлении данных, что приводит к полной перерисовке DOM-слоя.

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

  • создание overlay один раз
  • обновление только внутренних данных
  • избегание повторного вызова setMap(map)
class CustomOverlay extends google.maps.OverlayView {
  draw() {
    const projection = this.getProjection();
    const pos = projection.fromLatLngToDivPixel(this.position);
    this.div.style.left = pos.x + "px";
    this.div.style.top = pos.y + "px";
  }

  updatePosition(position) {
    this.position = position;
    this.draw();
  }
}

Ключевой момент — draw() должен работать только с изменёнными данными, без пересоздания DOM.

Снижение нагрузки на события карты

События bounds_changed, zoom_changed, center_changed могут генерироваться с высокой частотой.

Антипаттерн:

map.addListener("bounds_changed", () => {
  updateMarkers();
});

Это приводит к множественным обновлениям при одном жесте пользователя.

Оптимизация — привязка к стабильным событиям:

  • idle вместо bounds_changed
  • debounce обработчиков
map.addListener("idle", () => {
  updateMarkers();
});

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

Современный подход предполагает использование AdvancedMarkerElement, который более эффективно управляет DOM-структурой и снижает стоимость обновлений.

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

  • меньшая нагрузка на DOM
  • гибкое обновление содержимого
  • оптимизированная интеграция с WebGL слоем
const marker = new google.maps.marker.AdvancedMarkerElement({
  map,
  position: { lat: 50.45, lng: 30.52 },
});

Обновление позиции не требует пересоздания элемента, что уменьшает число перерисовок.

Изоляция вычислений от рендера карты

Частая ошибка — выполнение тяжёлых вычислений внутри событий карты.

Антипаттерн:

map.addListener("bounds_changed", () => {
  heavyGeoCalculation();
});

Это блокирует рендеринг и увеличивает задержки перерисовки.

Оптимальный подход:

  • вынос вычислений в отдельный слой
  • использование Web Worker при больших объёмах данных
  • кэширование результатов

Стабилизация ссылок на данные

При интеграции с React, Vue или аналогичными системами частая причина перерисовок — изменение ссылок на массивы и объекты.

Антипаттерн:

setMarkers([...newMarkers]);

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

Оптимизация — мутирование существующих структур или диффинг:

  • обновление по id
  • переиспользование объектов Marker
  • минимизация изменений state

Контроль визуальных обновлений DOM-оверлеев

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

  • изменение style.transform вместо пересоздания элемента
  • изменение текста без замены node
  • избегание innerHTML при частых обновлениях
label.textContent = newValue;
markerDiv.style.transform = `translate(${x}px, ${y}px)`;

Любая замена DOM-узла приводит к повторной компоновке слоя карты.

Снижение стоимости fitBounds через буферизацию

При потоковых данных границы часто пересчитываются многократно.

Решение — накопление точек и редкое обновление:

let bounds = new google.maps.LatLngBounds();

function addPoint(p) {
  bounds.extend(p);
}

setInterval(() => {
  map.fitBounds(bounds);
}, 1000);

Это уменьшает количество перерасчётов zoom и tile layout.

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

При работе с проекцией координат:

const projection = overlay.getProjection();

повторные вызовы fromLatLngToDivPixel можно кешировать при неизменном zoom и center.

Стратегия:

  • кешировать результаты при одинаковом zoom
  • пересчитывать только при zoom_changed

Это снижает нагрузку на вычисление экранных координат и уменьшает количество визуальных обновлений overlay-слоя.