Batch запросы

Современные веб-картографические приложения редко ограничиваются отображением одного источника данных. Карта может одновременно загружать геометрию объектов, стили, изображения, статистику, информацию о пробках, погодных условиях, пользовательских данных и результаты пространственных запросов. При большом количестве отдельных HTTP-запросов возрастает нагрузка на сеть, увеличивается задержка отображения данных и ухудшается пользовательский опыт.

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


Назначение пакетных запросов

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

Например:

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

Без пакетирования приложение может выполнять десятки запросов:

fetch('/api/object/1');
fetch('/api/object/2');
fetch('/api/object/3');
fetch('/api/object/4');
fetch('/api/object/5');

Подобный подход создает:

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

Вместо этого используется единый пакетный запрос:

fetch('/api/batch', {
    method: 'POST',
    body: JSON.stringify({
        ids: [1, 2, 3, 4, 5]
    })
});

Сервер возвращает данные сразу по всем объектам.


Архитектура пакетных запросов

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

Mapbox GL JS
       │
       ▼
Batch API
       │
 ┌─────┼─────┐
 ▼     ▼     ▼
DB    Cache  GIS

Карта отправляет один запрос.

Сервер:

  1. получает список операций;
  2. выполняет их параллельно;
  3. объединяет результаты;
  4. возвращает единый ответ.

Пакетная загрузка данных для источников GeoJSON

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

Вместо последовательной загрузки:

const roads = await fetch('/roads');
const buildings = await fetch('/buildings');
const parks = await fetch('/parks');

используется единый запрос:

const response = await fetch('/api/map-data');

const data = await response.json();

Ответ сервера:

{
    "roads": {},
    "buildings": {},
    "parks": {}
}

После получения данные добавляются в карту:

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

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

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

Загрузка данных по текущему экстенту карты

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

Получение границ карты:

const bounds = map.getBounds();

Формирование пакетного запроса:

const response = await fetch('/api/batch-extent', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        bbox: [
            bounds.getWest(),
            bounds.getSouth(),
            bounds.getEast(),
            bounds.getNorth()
        ]
    })
});

Ответ может содержать сразу несколько наборов объектов:

{
    "restaurants": {},
    "hotels": {},
    "stations": {}
}

Batch-загрузка при перемещении карты

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

Событие:

map.on('moveend', loadData);

Функция загрузки:

async function loadData() {
    const bounds = map.getBounds();

    const response = await fetch('/api/batch-map-data', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            west: bounds.getWest(),
            south: bounds.getSouth(),
            east: bounds.getEast(),
            north: bounds.getNorth()
        })
    });

    const data = await response.json();

    map.getSource('places')
       .setData(data.places);

    map.getSource('roads')
       .setData(data.roads);
}

Такой подход особенно эффективен для больших картографических систем.


Batch-запросы и кластеризация

При отображении тысяч точек может потребоваться дополнительная информация для каждого кластера.

Получение видимых кластеров:

const features = map.queryRenderedFeatures({
    layers: ['clusters']
});

Формирование списка идентификаторов:

const clusterIds = features.map(
    feature => feature.properties.cluster_id
);

Отправка единого запроса:

const response = await fetch('/api/clusters/info', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        clusters: clusterIds
    })
});

Ответ:

{
    "101": {
        "sales": 1000
    },
    "102": {
        "sales": 2500
    }
}

Получение информации по нескольким объектам

После выбора нескольких объектов пользовательского интерфейса может потребоваться дополнительная информация.

Сбор идентификаторов:

const selectedIds = features.map(
    feature => feature.properties.id
);

Пакетный запрос:

const response = await fetch('/api/features/details', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        ids: selectedIds
    })
});

Сервер возвращает данные одним ответом.


Batch-загрузка изображений

Многие карты используют кастомные маркеры.

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

fetch('/icon1.png');
fetch('/icon2.png');
fetch('/icon3.png');

Пакетный вариант:

const response = await fetch('/api/icons');

Ответ:

{
    "hospital": "...",
    "school": "...",
    "airport": "..."
}

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

map.addImage('hospital', imageData);

Параллельные запросы через Promise.all

Даже если сервер не поддерживает Batch API, можно организовать клиентское пакетирование.

Пример:

const requests = [
    fetch('/roads'),
    fetch('/buildings'),
    fetch('/parks')
];

const responses = await Promise.all(requests);

Получение данных:

const datasets = await Promise.all(
    responses.map(item => item.json())
);

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


Batch-запросы через GraphQL

В крупных приложениях Mapbox GL JS часто работает вместе с GraphQL.

Пример запроса:

const query = `
{
    roads {
        id
        geometry
    }

    buildings {
        id
        geometry
    }

    parks {
        id
        geometry
    }
}
`;

Отправка:

const response = await fetch('/graphql', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ query })
});

В результате один запрос возвращает сразу несколько наборов пространственных данных.


Batch-загрузка тайлов собственной инфраструктуры

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

Стандартная схема:

/z/x/y.pbf
/z/x/y.pbf
/z/x/y.pbf

Пакетная схема:

/api/tiles

Тело запроса:

{
    "tiles": [
        "10/523/344",
        "10/524/344",
        "10/525/344"
    ]
}

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


Обработка ошибок в пакетных запросах

Важно помнить, что ошибка одного элемента не должна приводить к потере всех данных.

Неудачный вариант:

{
    "success": false
}

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

{
    "roads": {
        "success": true
    },

    "buildings": {
        "success": false,
        "error": "Database unavailable"
    },

    "parks": {
        "success": true
    }
}

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

Пример:

if (data.buildings.success) {
    updateBuildings(data.buildings.data);
}

Ограничение размера пакета

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

Например:

{
    ids: [1 ... 50000]
}

Лучше разбивать данные на части.

Функция разбиения массива:

function chunk(array, size) {
    const result = [];

    for (let i = 0; i < array.length; i += size) {
        result.push(
            array.slice(i, i + size)
        );
    }

    return result;
}

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

const batches = chunk(ids, 500);

Кэширование пакетных ответов

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

Пример:

const cache = new Map();

Проверка:

if (cache.has(key)) {
    return cache.get(key);
}

Сохранение:

cache.set(key, response);

В картографических приложениях это особенно полезно при повторном просмотре одних и тех же областей карты.


Batch-запросы и Web Workers

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

Обработка ответа в Worker:

worker.postMessage(data);

Получение результата:

worker.onmess age = event => {
    updateMap(event.data);
};

Такой подход снижает нагрузку на интерфейс и делает работу карты более плавной.


Оптимизация пакетных запросов

Наиболее эффективная стратегия включает несколько компонентов одновременно:

  • объединение связанных запросов;
  • загрузку данных только для видимой области;
  • использование кэширования;
  • ограничение размеров пакетов;
  • параллельную серверную обработку;
  • применение Web Workers;
  • передачу данных в бинарных форматах;
  • использование HTTP/2 и HTTP/3;
  • агрегацию пространственных запросов на стороне сервера;
  • обновление только изменившихся объектов.

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