Подгрузка по требованию

Mapbox GL JS реализует рендеринг карты через WebGL и работает с векторными и растровыми тайлами, которые загружаются динамически в зависимости от текущего состояния камеры. Подгрузка по требованию (lazy/on-demand loading) является базовым механизмом производительности, позволяющим минимизировать сетевой трафик, ускорять инициализацию карты и снижать нагрузку на браузер.


Модель ленивой загрузки данных

В основе работы лежит принцип: данные запрашиваются только тогда, когда они становятся потенциально видимыми или необходимыми для рендера.

Основные уровни подгрузки:

  • Sources (источники данных) — регистрируются один раз и активируются при необходимости
  • Tiles (тайлы) — загружаются по сетке видимой области карты
  • Layers (слои) — рендерятся только при наличии данных и условий видимости
  • Assets (шрифты, спрайты, изображения) — загружаются асинхронно при первом использовании

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

Ограничение диапазона масштабов

Ключевой механизм — параметры minzoom и maxzoom:

map.addSource('cities', {
  type: 'vector',
  url: 'mapbox://examples.city-boundaries',
  minzoom: 5,
  maxzoom: 12
});

Поведение:

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

Геоданные по требованию через GeoJSON

Для динамической загрузки данных применяется GeoJSONSource:

map.addSource('dynamic-points', {
  type: 'geojson',
  data: {
    type: 'FeatureCollection',
    features: []
  }
});

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

fetch('/api/points?bbox=' + map.getBounds().toArray().flat())
  .then(res => res.json())
  .then(data => {
    map.getSource('dynamic-points').setData(data);
  });

Подгрузка данных по событию движения карты

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

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

map.on('moveend', () => {
  const bounds = map.getBounds();

  fetch(`/api/features?bbox=${bounds.toArray().flat().join(',')}`)
    .then(res => res.json())
    .then(data => {
      map.getSource('dynamic-points').setData(data);
    });
});

Особенности:

  • исключает лишние запросы во время панорамирования
  • синхронизирует данные с текущим viewport
  • снижает частоту сетевых операций

Дебаунсинг запросов

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

let timeout;

map.on('move', () => {
  clearTimeout(timeout);

  timeout = setTimeout(() => {
    const bounds = map.getBounds();

    fetch(`/api/features?bbox=${bounds.toArray().flat()}`)
      .then(res => res.json())
      .then(data => {
        map.getSource('dynamic-points').setData(data);
      });
  }, 300);
});

Проверка загрузки тайлов

Mapbox GL JS предоставляет механизмы контроля состояния источников.

isSourceLoaded

if (map.isSourceLoaded('cities')) {
  console.log('Источник уже загружен');
}

sourceData событие

map.on('sourcedata', (e) => {
  if (e.isSourceLoaded) {
    console.log('Источник полностью загружен');
  }
});

Управление слоями как механизмом ленивой визуализации

Слои можно включать и отключать в зависимости от контекста.

Динамическое добавление слоя

map.addLayer({
  id: 'city-layer',
  type: 'circle',
  source: 'dynamic-points',
  paint: {
    'circle-radius': 6,
    'circle-color': '#ff5500'
  }
});

Отключение слоя вместо удаления

map.setLayoutProperty('city-layer', 'visibility', 'none');

Подход предпочтительнее удаления:

  • не теряется конфигурация
  • не перезапрашиваются источники
  • быстрее повторная активация

Условная подгрузка через bounding box

Одним из наиболее эффективных методов является загрузка только видимой области.

function loadVisibleFeatures() {
  const bbox = map.getBounds();

  fetch('/api/features', {
    method: 'POST',
    body: JSON.stringify({ bbox })
  })
  .then(res => res.json())
  .then(data => {
    map.getSource('dynamic-points').setData(data);
  });
}

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

  • только актуальные координаты
  • снижение объема данных
  • масштабируемость API

Ленивая загрузка изображений и иконок

Mapbox GL JS поддерживает асинхронное добавление ресурсов в sprite-подобную систему.

loadImage

map.loadImage('/icons/marker.png', (error, image) => {
  if (!error) {
    map.addImage('custom-marker', image);
  }
});

Поведение:

  • изображение загружается только при необходимости слоя
  • повторное использование кэшируется внутри стиля

Подгрузка шрифтов и glyphs

Шрифты загружаются автоматически при появлении текстовых слоев:

map.addLayer({
  id: 'labels',
  type: 'symbol',
  source: 'cities',
  layout: {
    'text-field': ['get', 'name']
  }
});

Механизм:

  • glyphs запрашиваются по диапазону символов
  • загрузка происходит по мере появления текста в viewport
  • исключается предварительная загрузка всего алфавита

Ограничение области рендера

renderWorldCopies

map.setRenderWorldCopies(false);

Влияние:

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

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

setStyle как механизм ленивой смены данных

map.setStyle('mapbox://styles/mapbox/dark-v11');

Особенности:

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

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

map.on('idle', () => {
  console.log('Все видимые ресурсы загружены');
});

Смысл:

  • фиксирует момент, когда нет активных сетевых запросов
  • используется для триггера дополнительных подгрузок

Динамическая фильтрация как альтернатива загрузке

Вместо загрузки новых данных можно изменять фильтры слоя:

map.setFilter('city-layer', ['>', ['get', 'population'], 100000]);

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

  • не требует повторных запросов
  • работает на уже загруженных данных
  • мгновенное обновление рендера

Фича-состояния как способ ленивого обновления

map.setFeatureState(
  { source: 'cities', id: 123 },
  { selected: true }
);

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

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

Стратегии оптимизации подгрузки

1. Загрузка по уровню зума

  • разные наборы данных для разных масштабов
  • уменьшение детализации на дальних уровнях

2. Загрузка по географическим зонам

  • деление данных на регионы
  • подгрузка при пересечении границ

3. Загрузка по пользовательскому поведению

  • активировать данные при hover/click
  • использовать queryRenderedFeatures перед запросом
map.on('click', (e) => {
  const features = map.queryRenderedFeatures(e.point, {
    layers: ['city-layer']
  });

  if (features.length) {
    loadDetails(features[0].properties.id);
  }
});

Контроль повторных запросов

Типичная проблема — дублирование загрузок при частых событиях камеры.

Решение через кеширование:

const cache = new Set();

function loadTile(id) {
  if (cache.has(id)) return;

  cache.add(id);

  fetch(`/api/tile/${id}`)
    .then(res => res.json())
    .then(data => {
      // обработка
    });
}

Итоговые паттерны архитектуры подгрузки

  • источники регистрируются один раз и переиспользуются
  • данные обновляются только по изменению viewport
  • слои переключаются через visibility вместо удаления
  • изображения и шрифты загружаются лениво
  • API синхронизируется с bounding box
  • события moveend, idle, sourcedata используются как триггеры
  • кэширование предотвращает повторные сетевые операции