Оптимизация количества запросов

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

Избыточное количество запросов приводит к нескольким проблемам:

  • увеличению времени загрузки карты;
  • повышенному потреблению трафика;
  • росту нагрузки на серверы;
  • замедлению отображения интерфейса;
  • достижению лимитов API быстрее запланированного.

Оптимизация запросов является одной из ключевых задач при разработке масштабируемых картографических приложений.


Какие запросы выполняет Mapbox GL JS

При инициализации карты библиотека может обращаться к нескольким типам ресурсов:

Стиль карты

const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v12'
});

Запрашивается JSON-файл стиля, содержащий описание всех слоев и источников.


Тайлы

Для каждого уровня масштабирования загружаются отдельные тайлы.

Пример URL:

https://api.mapbox.com/v4/mapbox.mapbox-streets-v8/12/2384/1452.vector.pbf

Каждый новый участок карты требует загрузки соответствующих тайлов.


Спрайты

Спрайты содержат иконки для отображения объектов.

sprite.json
sprite.png

Обычно загружаются один раз при загрузке стиля.


Шрифты

Для отображения подписей карта запрашивает наборы глифов.

fonts/v1/...

Каждый используемый шрифт может создавать дополнительные запросы.


Источники данных

Дополнительные GeoJSON-источники также требуют загрузки.

map.addSource('cities', {
    type: 'geojson',
    data: '/data/cities.geojson'
});

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

Одним из наиболее эффективных способов сокращения запросов является корректное кеширование.

Mapbox предоставляет HTTP-заголовки, позволяющие браузеру повторно использовать уже загруженные данные.

При повторном открытии страницы:

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

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

Плохой пример:

fetch(`/data/cities.geojson?t=${Date.now()}`);

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

Лучше использовать версионирование:

fetch('/data/cities.geojson?v=3');

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

Частая ошибка — создание большого количества отдельных источников.

Неэффективный вариант:

map.addSource('restaurants', {...});
map.addSource('hotels', {...});
map.addSource('parks', {...});
map.addSource('shops', {...});

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

Гораздо эффективнее объединять данные:

map.addSource('places', {
    type: 'geojson',
    data: '/data/places.geojson'
});

Далее объекты разделяются через фильтры слоев.

map.addLayer({
    id: 'restaurants',
    source: 'places',
    filter: ['==', ['get', 'type'], 'restaurant']
});

Такой подход уменьшает количество сетевых обращений и упрощает управление данными.


Объединение GeoJSON-данных

Множество небольших файлов создают множество запросов.

Плохой вариант:

/data/city1.geojson
/data/city2.geojson
/data/city3.geojson
/data/city4.geojson

Каждый файл требует отдельного обращения к серверу.

Лучше объединить данные:

/data/all-cities.geojson

Подобная оптимизация особенно заметна на мобильных устройствах и медленных соединениях.


Использование векторных тайлов вместо больших GeoJSON

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

Например:

map.addSource('roads', {
    type: 'geojson',
    data: '/data/roads.geojson'
});

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

Лучшим решением становятся векторные тайлы.

map.addSource('roads', {
    type: 'vector',
    url: 'mapbox://username.roads'
});

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

  • данные загружаются частями;
  • передаются только нужные участки карты;
  • уменьшается сетевой трафик;
  • сокращается время загрузки.

Ограничение области отображения карты

Если пользователь может перемещаться по всему миру, библиотека потенциально будет загружать огромное количество тайлов.

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

const bounds = [
    [36.0, 54.0],
    [39.0, 56.0]
];

const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v12',
    maxBounds: bounds
});

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

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

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

Каждый уровень масштабирования требует собственных тайлов.

Если разрешить масштабирование от 0 до 22:

minZoom: 0,
maxZoom: 22

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

Для городской карты разумнее:

minZoom: 10,
maxZoom: 18

Такой подход уменьшает количество запрашиваемых данных.


Использование параметров minzoom и maxzoom для слоев

Слои не обязательно должны существовать на всех масштабах.

Неоптимальный вариант:

map.addLayer({
    id: 'buildings',
    source: 'buildings'
});

Лучше явно задавать диапазон отображения.

map.addLayer({
    id: 'buildings',
    source: 'buildings',
    minzoom: 14,
    maxzoom: 22
});

При низких масштабах слой не участвует в обработке и не требует связанных данных.


Ленивая загрузка данных

Не все данные нужны сразу после открытия страницы.

Часто полезно загружать их только по требованию.

Например, после достижения определенного масштаба.

map.on('zoomend', () => {
    if (map.getZoom() >= 12 && !map.getSource('cities')) {
        map.addSource('cities', {
            type: 'geojson',
            data: '/data/cities.geojson'
        });
    }
});

Такой механизм существенно уменьшает первоначальное количество запросов.


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

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

GeoJSON-источник может автоматически кластеризоваться.

map.addSource('places', {
    type: 'geojson',
    data: '/data/places.geojson',
    cluster: true,
    clusterRadius: 50
});

Кластеризация не уменьшает число запросов напрямую, однако позволяет:

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

Избегание постоянного обновления GeoJSON

Часто встречается ошибка регулярной полной перезагрузки источника.

Плохой пример:

setInterval(() => {
    map.getSource('cars').setData('/cars.geojson');
}, 1000);

Файл загружается каждую секунду.

Более эффективный подход — получать только изменения.

fetch('/cars-updates');

или использовать потоковые технологии:

  • WebSocket;
  • Server-Sent Events;
  • MQTT через веб-шлюзы.

Повторное использование источников

Один источник может использоваться множеством слоев.

Неэффективно:

map.addSource('roads-red', {...});
map.addSource('roads-blue', {...});
map.addSource('roads-green', {...});

Лучше:

map.addSource('roads', {...});

и несколько слоев:

map.addLayer({
    id: 'roads-red',
    source: 'roads'
});

map.addLayer({
    id: 'roads-blue',
    source: 'roads'
});

Количество запросов при этом не увеличивается.


Дебаунсинг пользовательских действий

Иногда приложение выполняет запросы после каждого движения карты.

Плохой вариант:

map.on('move', loadData);

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

Лучше использовать дебаунсинг.

let timeout;

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

    timeout = setTimeout(() => {
        loadData();
    }, 300);
});

Количество запросов может уменьшиться в десятки раз.


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

Еще более эффективное решение — выполнять запрос только после завершения перемещения.

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

Пока пользователь перемещает карту, обращения к серверу не выполняются.


Загрузка данных только внутри текущего окна карты

Часто нет необходимости получать все объекты сразу.

Границы текущего просмотра можно использовать для серверной фильтрации.

const bounds = map.getBounds();

Получение координат:

const north = bounds.getNorth();
const south = bounds.getSouth();
const east = bounds.getEast();
const west = bounds.getWest();

Запрос:

fetch(
    `/api/places?n=${north}&s=${south}&e=${east}&w=${west}`
);

Сервер возвращает только объекты внутри видимой области.

Это один из наиболее эффективных способов уменьшения трафика.


Предварительная генерация тайлов

При работе с собственными картографическими данными выгодно заранее создавать тайлы.

Неэффективный вариант:

GeoJSON → сервер → клиент

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

GeoJSON → Tippecanoe → MBTiles → Vector Tiles

Предварительно сгенерированные тайлы:

  • меньше по размеру;
  • быстрее передаются;
  • требуют меньше запросов.

Сокращение количества используемых шрифтов

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

Неэффективно:

text-font: [
    'Open Sans Regular',
    'Arial Unicode MS Regular',
    'Roboto Regular',
    'Roboto Bold'
]

Лучше ограничиваться минимально необходимым набором.

text-font: [
    'Open Sans Regular'
]

Это сокращает количество сетевых обращений к серверу шрифтов.


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

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

Плохой подход:

icon-image: 'marker1.png'
icon-image: 'marker2.png'
icon-image: 'marker3.png'

Предпочтительно использовать единый спрайт.

sprite.png
sprite.json

Все изображения загружаются одним набором ресурсов.


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

Для анализа эффективности используются инструменты разработчика браузера.

Вкладка Network позволяет определить:

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

Особое внимание следует уделять:

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

Практическая стратегия оптимизации

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

  1. Использовать векторные тайлы вместо больших GeoJSON.
  2. Объединять данные в минимальное количество источников.
  3. Ограничивать диапазоны масштабирования.
  4. Ограничивать область перемещения карты.
  5. Загружать данные лениво.
  6. Использовать moveend вместо move.
  7. Применять дебаунсинг пользовательских действий.
  8. Настраивать корректное HTTP-кеширование.
  9. Загружать объекты только внутри текущего viewport.
  10. Повторно использовать источники и спрайты.

Комплексное применение этих методов позволяет сократить количество сетевых запросов в несколько раз, существенно уменьшить потребление трафика и обеспечить стабильную работу Mapbox GL JS даже при больших объемах картографических данных.