Hydration на клиенте

Hydration в контексте Mapbox GL JS обозначает процесс восстановления интерактивной карты на клиенте после серверного рендеринга или первичной отрисовки статического контейнера. В отличие от классических DOM-приложений, карта представляет собой не только разметку, но и WebGL-контекст, состояние камеры, загруженные тайлы, стиль, источники данных и набор слоёв. Поэтому «гидратация» здесь означает не только повторную инициализацию, но и аккуратное восстановление всего графа состояния карты без пересоздания логики с нуля.

Карта в Mapbox GL JS не является декларативным UI в чистом виде. Каждый экземпляр map инкапсулирует:

  • WebGL renderer и контекст
  • текущий стиль (style JSON)
  • источники данных (sources)
  • слои (layers)
  • состояние камеры (center, zoom, bearing, pitch)
  • взаимодействия (handlers, events)

При серверном рендеринге можно отдать только статический контейнер:

<div id="map"></div>

На клиенте этот контейнер становится точкой гидратации, где создаётся полноценный объект карты:

import mapboxgl from "mapbox-gl";

const map = new mapboxgl.Map({
  container: "map",
  style: "mapbox://styles/mapbox/streets-v12",
  center: [69.6, 42.3],
  zoom: 10
});

Проблема возникает, если на сервере уже был сформирован визуальный «снимок» состояния: камера, слои, фильтры. Клиентская инициализация должна восстановить это состояние без расхождений.

Ограничения SSR и необходимость отложенной инициализации

Mapbox GL JS зависит от браузерных API:

  • window
  • document
  • WebGL context
  • requestAnimationFrame

При SSR эти сущности отсутствуют, поэтому прямое создание карты приводит к ошибкам. Гидратация всегда переносится в клиентский слой:

let map;

if (typeof window !== "undefined") {
  map = new mapboxgl.Map({
    container: "map",
    style: window.__INITIAL_MAP_STYLE__,
    center: window.__INITIAL_CENTER__,
    zoom: window.__INITIAL_ZOOM__
  });
}

Ключевой принцип: сервер передаёт только сериализованное состояние, клиент восстанавливает интерактивность.

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

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

  • style URL или JSON
  • координаты центра
  • zoom, bearing, pitch
  • активные источники данных
  • состояние UI слоёв (visibility, filter)
  • выбранные объекты (feature state)

Пример структуры:

window.__MAP_STATE__ = {
  style: "mapbox://styles/mapbox/dark-v11",
  center: [69.6, 42.3],
  zoom: 11.2,
  bearing: 20,
  pitch: 35,
  activeLayers: ["roads", "buildings"]
};

Восстановление камеры без скачков

При гидратации критично избежать визуального «прыжка» карты. Типичная ошибка — инициализация с дефолтными значениями и последующий setView.

Корректный подход — передача состояния сразу в конструктор:

const state = window.__MAP_STATE__;

const map = new mapboxgl.Map({
  container: "map",
  style: state.style,
  center: state.center,
  zoom: state.zoom,
  bearing: state.bearing,
  pitch: state.pitch,
  preserveDrawingBuffer: true
});

Если состояние доступно только после инициализации, используется блокировка рендера:

map.once("load", () => {
  map.jumpTo({
    center: state.center,
    zoom: state.zoom,
    bearing: state.bearing,
    pitch: state.pitch
  });
});

Однако jumpTo после первого кадра создаёт дополнительный рендер, поэтому предпочтителен ранний injection состояния.

Гидратация слоёв и источников данных

Слои и источники в Mapbox GL JS не являются автоматически синхронизируемыми сущностями. При восстановлении состояния важно соблюдать порядок:

  1. Добавление источников (addSource)
  2. Добавление слоёв (addLayer)
  3. Применение фильтров и layout properties
  4. Восстановление visibility

Пример:

map.on("load", () => {
  const state = window.__MAP_STATE__;

  state.sources.forEach(src => {
    map.addSource(src.id, src.data);
  });

  state.layers.forEach(layer => {
    map.addLayer(layer);
  });

  state.layers.forEach(layer => {
    if (layer.visibility === "none") {
      map.setLayoutProperty(layer.id, "visibility", "none");
    }
  });
});

Проблема дублирования и повторной инициализации

Частая ошибка гидратации — повторное создание карты в одном контейнере. WebGL контекст не допускает безопасного «перезапуска» без освобождения ресурсов.

Перед созданием нового экземпляра требуется явное уничтожение:

if (map) {
  map.remove();
}

remove() освобождает:

  • WebGL context
  • event listeners
  • tile cache
  • internal workers

Игнорирование этого шага приводит к утечкам памяти и блокировке GPU ресурсов.

Синхронизация состояния между сервером и клиентом

Гидратация подразумевает согласование двух моделей состояния:

  • серверная (snapshot)
  • клиентская (runtime)

Конфликт возникает, если стиль изменился между рендером и гидратацией. В этом случае Mapbox пересобирает граф слоёв, что может привести к мерцанию.

Для стабилизации используется фиксация версии стиля:

const style = window.__MAP_STATE__.styleVersioned;

map.setStyle(style);

После setStyle необходимо повторно восстановить источники и слои, так как стиль сбрасывает их состояние.

Асинхронная загрузка стиля и блокировка гидратации

Стиль в Mapbox GL JS загружается асинхронно. Это создаёт гонку между:

  • восстановлением состояния
  • загрузкой style JSON
  • инициализацией WebGL pipeline

Корректная схема:

map.on("style.load", () => {
  restoreSources(map);
  restoreLayers(map);
  restoreCamera(map);
});

Событие style.load является точкой, где граф рендера гарантированно готов к модификации.

Гидратация в SPA и повторное монтирование

В SPA-архитектурах контейнер карты может уничтожаться и создаваться повторно. При этом важно различать:

  • первичную гидратацию
  • ремоунт компонента
  • восстановление из кеша состояния

Если контейнер сохраняется, возможен паттерн «reuse instance»:

if (map.getContainer().isConnected) {
  map.resize();
} else {
  map = createMap();
}

resize() критичен при изменении размеров контейнера после гидратации, особенно при flex/grid layout.

Оптимизация гидратации через lazy initialization

Инициализация карты может быть отложена до появления контейнера в viewport. Это уменьшает стоимость первичного рендера:

const observer = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) {
    initMap();
    observer.disconnect();
  }
});

observer.observe(document.getElementById("map"));

Такой подход снижает нагрузку на WebGL и ускоряет гидратацию страницы в целом.

Контроль целостности состояния

При сложных картах важно проверять консистентность:

  • существует ли слой перед обновлением
  • загружен ли источник перед фильтрацией
  • завершена ли загрузка стиля
if (map.getLayer("roads")) {
  map.setFilter("roads", ["==", "type", "primary"]);
}

Отсутствие проверок приводит к ошибкам гидратации при частичной загрузке.

Гидратация камеры и предотвращение визуальных артефактов

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

Минимизация артефактов достигается через:

  • идентичные параметры инициализации
  • отключение анимации при первом кадре
  • использование essential: true
map.jumpTo({
  center: state.center,
  zoom: state.zoom,
  essential: true
});

Итоговая модель гидратации

Гидратация в Mapbox GL JS сводится к согласованному восстановлению трёх уровней:

  • граф рендера (style, layers, sources)
  • геометрического состояния (camera)
  • интерактивного состояния (events, UI flags)

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