При работе с интерактивными картами производительность зависит не только от скорости рендеринга, но и от количества сетевых запросов. Каждое перемещение карты, изменение масштаба или обновление данных может приводить к загрузке новых тайлов, стилей, шрифтов, спрайтов и внешних источников данных.
Избыточное количество запросов приводит к нескольким проблемам:
Оптимизация запросов является одной из ключевых задач при разработке масштабируемых картографических приложений.
При инициализации карты библиотека может обращаться к нескольким типам ресурсов:
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']
});
Такой подход уменьшает количество сетевых обращений и упрощает управление данными.
Множество небольших файлов создают множество запросов.
Плохой вариант:
/data/city1.geojson
/data/city2.geojson
/data/city3.geojson
/data/city4.geojson
Каждый файл требует отдельного обращения к серверу.
Лучше объединить данные:
/data/all-cities.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
});
Преимущества:
Каждый уровень масштабирования требует собственных тайлов.
Если разрешить масштабирование от 0 до 22:
minZoom: 0,
maxZoom: 22
будет потенциально доступно огромное количество тайлов.
Для городской карты разумнее:
minZoom: 10,
maxZoom: 18
Такой подход уменьшает количество запрашиваемых данных.
Слои не обязательно должны существовать на всех масштабах.
Неоптимальный вариант:
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
});
Кластеризация не уменьшает число запросов напрямую, однако позволяет:
Часто встречается ошибка регулярной полной перезагрузки источника.
Плохой пример:
setInterval(() => {
map.getSource('cars').setData('/cars.geojson');
}, 1000);
Файл загружается каждую секунду.
Более эффективный подход — получать только изменения.
fetch('/cars-updates');
или использовать потоковые технологии:
Один источник может использоваться множеством слоев.
Неэффективно:
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);
});
Количество запросов может уменьшиться в десятки раз.
Еще более эффективное решение — выполнять запрос только после завершения перемещения.
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 позволяет определить:
Особое внимание следует уделять:
Для большинства производственных проектов эффективной считается следующая схема:
moveend вместо move.Комплексное применение этих методов позволяет сократить количество сетевых запросов в несколько раз, существенно уменьшить потребление трафика и обеспечить стабильную работу Mapbox GL JS даже при больших объемах картографических данных.