Оптимизация запросов

MapLibre GL JS опирается на векторные и растровые тайлы, каждый из которых загружается по мере необходимости в зависимости от текущего viewport, зума и состояния стиля. Основная причина избыточной нагрузки — неконтролируемое расширение источников данных и слоёв, приводящее к лавинообразному росту HTTP-запросов.

Ключевой механизм оптимизации заключается в ограничении числа одновременно активных источников (sources) и слоёв (layers). Каждый источник в стиле может инициировать собственные запросы к тайловому серверу, поэтому архитектура стиля должна быть строго нормализована.

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

  • объединение логически близких данных в один vector tile source
  • использование общих tileset’ов вместо множества мелких GeoJSON-источников
  • настройка minzoom и maxzoom на уровне источника
map.addSource('roads', {
  type: 'vector',
  tiles: ['https://tiles.example.com/roads/{z}/{x}/{y}.pbf'],
  minzoom: 5,
  maxzoom: 14
});

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


Оптимизация фильтрации данных в слоях

Фильтрация через filter в слоях MapLibre выполняется на стороне клиента после загрузки тайла. Избыточная или сложная фильтрация увеличивает нагрузку на main thread и снижает FPS при взаимодействии с картой.

Рекомендации:

  • перенос логики фильтрации на сервер генерации тайлов
  • минимизация использования сложных логических операторов (any, all, вложенные условия)
  • избегание динамически изменяемых фильтров при частых событиях перемещения карты

Плохая практика:

filter: ['all',
  ['==', ['get', 'type'], 'road'],
  ['>', ['get', 'priority'], 3],
  ['in', ['get', 'status'], 'active', 'planned', 'construction']
]

Если такие условия часто изменяются, выгоднее разделить данные на несколько слоёв уже в tileset.


Снижение количества запросов через правильную структуру источников

Каждый source в стиле MapLibre — потенциальный источник сетевой активности. Избыточное дробление данных приводит к росту количества параллельных запросов и перегрузке браузера.

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

  • один источник — один тематический слой данных (roads, buildings, water)
  • избегание дублирования одинаковых tileset URL
  • использование promoteId для унификации идентификаторов вместо дублирующих источников
map.addSource('buildings', {
  type: 'vector',
  tiles: ['https://tiles.example.com/buildings/{z}/{x}/{y}.pbf'],
  promoteId: 'id'
});

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


Кэширование и повторное использование тайлов

MapLibre GL JS использует внутренний кэш тайлов, однако эффективность кэширования зависит от стабильности URL и стратегии серверной отдачи данных.

Критические условия эффективного кэша:

  • неизменяемые URL тайлов (без случайных query string параметров)
  • корректные HTTP заголовки Cache-Control и ETag
  • использование CDN с геораспределённой доставкой

Плохой паттерн:

/tiles/{z}/{x}/{y}.pbf?timestamp=123456

Любое изменение параметра инвалидирует кэш и приводит к повторным запросам.


Ограничение перерисовок через события карты

Многие лишние запросы косвенно вызваны частыми обновлениями стиля или данных при событиях карты (move, moveend, zoom, render). Неправильная привязка обработчиков приводит к постоянным пересборкам источников и повторной загрузке тайлов.

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

  • move — только визуальные изменения без запросов
  • moveend — триггер для загрузки дополнительных данных
  • idle — безопасная точка для тяжёлых операций

Пример анти-паттерна:

map.on('move', () => {
  updateData(); // вызывает новые запросы на каждый пиксель движения
});

Оптимизированный вариант:

map.on('moveend', () => {
  updateData();
});

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

Метод queryRenderedFeatures может быть источником значительной нагрузки при частом вызове во время перемещения карты или взаимодействия с курсором. Несмотря на то что он работает локально, он инициирует обход внутренних структур тайлов и слоёв.

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

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

let timeout;

map.on('mousemove', (e) => {
  clearTimeout(timeout);
  timeout = setTimeout(() => {
    const features = map.queryRenderedFeatures(e.point);
    process(features);
  }, 100);
});

Снижение частоты обращений уменьшает нагрузку на main thread и ускоряет реакцию интерфейса.


Уменьшение объёма данных в GeoJSON источниках

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

Методы оптимизации:

  • упрощение геометрии (simplification)
  • разбиение на тайлы вместо единого большого GeoJSON
  • использование cluster для точечных данных

Пример кластеризации:

map.addSource('points', {
  type: 'geojson',
  data: 'https://example.com/points.geojson',
  cluster: true,
  clusterMaxZoom: 12,
  clusterRadius: 50
});

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


Оптимизация работы с layout и paint свойствами

Каждое изменение layout или paint свойств слоя вызывает перерасчёт стиля и может приводить к повторной выборке данных. Частые динамические изменения стилей создают эффект псевдо-постоянных запросов.

Рекомендации:

  • избегать постоянного вызова setPaintProperty в циклах
  • группировать изменения через batch-обновления
  • использовать feature-state вместо прямой модификации слоёв
map.setFeatureState(
  { source: 'buildings', id: 42 },
  { hover: true }
);

feature-state обновляет визуализацию без перезагрузки тайлов и без сетевых запросов.


Сокращение числа слоёв и упрощение стиля

Каждый слой в стиле увеличивает стоимость рендеринга и косвенно влияет на количество обращений к данным источников. Особенно это критично при использовании большого количества символических (symbol) слоёв.

Стратегии оптимизации:

  • объединение визуально схожих слоёв
  • использование minzoom / maxzoom для отключения слоёв вне диапазона
  • сокращение количества text-field выражений с динамическими данными

Пример ограничения:

{
  id: 'labels',
  type: 'symbol',
  source: 'places',
  minzoom: 10,
  maxzoom: 18,
  layout: {
    'text-field': ['get', 'name']
  }
}

Контроль запросов через transformRequest

Функция transformRequest позволяет перехватывать все сетевые запросы MapLibre GL JS. Это ключевой инструмент для оптимизации, позволяющий внедрять кастомный кэш, подписывать запросы или перенаправлять их на CDN.

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

const map = new maplibregl.Map({
  container: 'map',
  style: 'style.json',
  transformRequest: (url, resourceType) => {
    return {
      url,
      headers: {
        'X-Requested-With': 'MapLibre'
      }
    };
  }
});

Также возможно маршрутизировать запросы к локальному кэшу или сервис-воркеру, снижая количество сетевых обращений.


Минимизация пересечений источников данных

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

Оптимальное решение — унификация tileset и переиспользование одного source для всех слоёв, работающих с одной географической областью.


Управление приоритетами загрузки

MapLibre GL JS использует внутренний приоритет загрузки тайлов, основанный на расстоянии от центра экрана и текущем зуме. Однако при большом количестве источников этот механизм может быть перегружен.

Снижение нагрузки достигается:

  • уменьшением количества одновременно видимых слоёв
  • ограничением renderWorldCopies при глобальных картах
  • отключением невидимых источников при выходе за пределы области интереса
map.setLayoutProperty('layer-id', 'visibility', 'none');

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