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

Архитектура рендеринга карты и источники перерисовок

HERE Technologies использует в JavaScript API гибридную модель рендеринга, где карта состоит из базового слоя тайлов, векторных/растровых данных и динамических объектов (маркерных слоёв, полигонов, линий и UI-оверлеев). Любое изменение состояния карты может приводить к частичной или полной перерисовке сценического графа.

Ключевые источники перерисовок:

  • изменение центра карты (setCenter)
  • изменение масштаба (setZoom)
  • добавление/удаление объектов (H.map.Object)
  • обновление свойств объектов (позиция, стиль)
  • изменения DOM-слоя UI (H.ui)
  • события pointer/mouse, вызывающие обновление состояния
  • обновление геометрии (polylines, polygons)

Минимизация перерисовок опирается на контроль этих триггеров и сокращение количества операций, влияющих на сцену.


Базовый принцип: группировка изменений состояния

Любая последовательность операций над картой должна рассматриваться как единая транзакция. Частые одиночные вызовы приводят к каскадному пересчёту сцены.

Проблемный паттерн:

map.setCenter({ lat: 52.5, lng: 13.4 });
map.setZoom(10);
map.addObject(marker);

Каждая операция вызывает отдельный цикл обновления.

Оптимизированный подход — агрегирование состояния:

map.getViewModel().setLookAtData({
  position: { lat: 52.5, lng: 13.4 },
  zoom: 10
});

map.addObject(marker);

Идея заключается в уменьшении количества пересчётов камеры и viewport.


Использование requestAnimationFrame для пакетных обновлений

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

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

points.forEach(p => {
  marker.setGeometry(p);
});

Оптимизация через кадрирование:

requestAnimationFrame(() => {
  const batch = [];
  points.forEach(p => batch.push(p));

  batch.forEach(p => marker.setGeometry(p));
});

При большом количестве объектов дополнительно применяется разбиение на чанки:

function updateInChunks(data, chunkSize = 50) {
  let index = 0;

  function step() {
    const end = Math.min(index + chunkSize, data.length);

    for (let i = index; i < end; i++) {
      markers[i].setGeometry(data[i]);
    }

    index = end;
    if (index < data.length) {
      requestAnimationFrame(step);
    }
  }

  requestAnimationFrame(step);
}

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

Объекты H.map.Marker являются частым источником нагрузки. Каждое изменение геометрии или иконки может инициировать перерасчёт слоя.

Повторное использование объектов (Object Pooling)

Создание новых маркеров дорогостояще. Вместо этого используется пул объектов:

const pool = [];

function getMarker() {
  return pool.pop() || new H.map.Marker({ lat: 0, lng: 0 });
}

function releaseMarker(marker) {
  marker.setGeometry({ lat: 0, lng: 0 });
  pool.push(marker);
}

Это снижает давление на garbage collector и уменьшает количество DOM/WebGL операций.


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

События движения карты генерируются с высокой частотой, особенно при drag и pointermove.

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

map.addEventListener('pointermove', e => {
  updateUI(e.currentPointer);
});

Оптимизация через throttle:

function throttle(fn, delay) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= delay) {
      last = now;
      fn(...args);
    }
  };
}

map.addEventListener('pointermove', throttle(e => {
  updateUI(e.currentPointer);
}, 50));

Debounce применяется для событий завершения взаимодействия:

function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

Группировка геометрии в контейнеры

Каждый H.map.Object добавленный отдельно увеличивает нагрузку на сцену. Вместо множества независимых объектов применяется группировка:

const group = new H.map.Group();

markers.forEach(m => group.addObject(m));

map.addObject(group);

Группы уменьшают количество пересчётов hit-testing и рендеринга.


Минимизация обновлений геометрии

Изменение геометрии (setGeometry) является дорогостоящей операцией. Векторные объекты пересчитываются полностью.

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

  • обновлять только изменившиеся точки
  • избегать повторного назначения идентичных координат
  • сравнивать значения перед обновлением
function updateMarker(marker, coords) {
  const current = marker.getGeometry();
  if (current.lat !== coords.lat || current.lng !== coords.lng) {
    marker.setGeometry(coords);
  }
}

Контроль перерисовки карты при изменении viewport

Изменения viewport через H.map.ViewPort или изменение размеров контейнера вызывают полный пересчёт сцены.

При динамическом UI важно минимизировать resize-события:

const resizeObserver = new ResizeObserver(() => {
  map.getViewPort().resize();
});

resizeObserver.observe(document.getElementById('mapContainer'));

Оптимизация заключается в предотвращении частых вызовов resize через debounce.


Использование событий с низкой частотой обновления

HERE Maps API предоставляет события разного уровня частоты:

  • mapviewchange — высокая частота
  • mapviewchangeend — завершение движения
  • pointermove — крайне частое

Оптимальная стратегия:

  • UI обновляется на mapviewchangeend
  • промежуточные вычисления игнорируются или throttled
map.addEventListener('mapviewchangeend', () => {
  updateVisibleObjects();
});

Управление слоями и порядок отрисовки

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

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

  • объединение логически связанных объектов в один слой
  • избегание глубокой иерархии H.map.Group
  • контроль z-index через минимальное количество изменений

Отключение лишних эффектов интерфейса

UI слой H.ui.UI может генерировать дополнительные перерисовки при включённых контролах:

  • скейлбар
  • инфобоксы
  • всплывающие элементы

Минимизация:

  • отключение ненужных контролов
  • использование кастомного UI без частых DOM-обновлений

Батчинг операций добавления и удаления объектов

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

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

data.forEach(d => map.addObject(new H.map.Marker(d)));

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

const group = new H.map.Group();

data.forEach(d => {
  group.addObject(new H.map.Marker(d));
});

map.addObject(group);

Снижение стоимости hit-testing

Hit-testing выполняется при каждом pointer-событии. Большое количество интерактивных объектов увеличивает нагрузку.

Подходы:

  • отключение интерактивности у статических объектов
  • уменьшение числа объектов с setInteractive(true)
  • группировка объектов
marker.setInteractive(false);

Кэширование стилей и иконок

Динамическое создание иконок вызывает повторные GPU/DOM операции.

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

  • переиспользование H.map.Icon
  • предсоздание наборов иконок
const icon = new H.map.Icon('/img/marker.png');

const marker = new H.map.Marker(coords, { icon });

Избежание лишних перерасчётов при изменении состояния приложения

Частая ошибка — связывание состояния приложения с картой напрямую, что приводит к цепным обновлениям.

Проблемный паттерн:

state.onChange(() => {
  map.setCenter(state.center);
});

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

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

Контроль жизненного цикла объектов карты

Удаление объектов должно сопровождаться явным освобождением ссылок:

map.removeObject(marker);
marker.dispose?.();
marker = null;

При массовых изменениях предпочтительно очищать группы:

group.removeAll();

Использование стратегий деградации детализации

При большом количестве объектов применяется адаптивная детализация:

  • уменьшение количества маркеров при низком zoom
  • агрегация точек в кластеры
  • скрытие второстепенных слоёв

Это напрямую уменьшает число перерисовок сцены.


Кластеризация как средство снижения нагрузки

При тысячах объектов рендер каждого маркера становится неоптимальным. Кластеризация объединяет их в агрегированные сущности, снижая количество draw calls и операций hit-testing.

const clusterProvider = new H.clustering.Provider(dataPoints);
const layer = new H.map.layer.ObjectLayer(clusterProvider);
map.addLayer(layer);

Синхронизация логики с рендер-циклом карты

Любые изменения должны учитываться в контексте кадра карты. Частые вызовы API вне синхронизации с render loop приводят к избыточным пересчётам сцены.

Оптимальный подход — централизованная очередь изменений:

const queue = [];

function scheduleUpdate(fn) {
  queue.push(fn);
}

function flush() {
  const batch = queue.splice(0);
  batch.forEach(fn => fn());
  requestAnimationFrame(flush);
}

requestAnimationFrame(flush);