Lazy loading (ленивая загрузка) — подход, при котором данные загружаются не целиком при инициализации карты, а только в тот момент, когда они действительно необходимы. Для веб-карт это особенно важно, поскольку геоданные могут содержать десятки тысяч объектов, занимать мегабайты трафика и существенно влиять на производительность браузера.
В контексте Mapbox GL JS ленивую загрузку применяют для:
Основная цель — уменьшить объем передаваемых данных и снизить нагрузку на процессор, память и сеть.
Рассмотрим ситуацию, когда сервер возвращает GeoJSON с 100 000 объектов.
fetch('/api/points')
.then(response => response.json())
.then(data => {
map.addSource('points', {
type: 'geojson',
data: data
});
});
Подобный подход приводит к нескольким проблемам:
Браузер ожидает получения всего файла перед отображением слоя.
Все объекты одновременно находятся в памяти JavaScript и движка Mapbox GL JS.
Каждый объект участвует в:
Пользователь редко просматривает всю карту сразу, однако загружаются все данные.
На практике используются несколько основных стратегий.
Подгружаются только объекты внутри текущей области просмотра.
Разные наборы данных загружаются на разных уровнях приближения.
Карта получает небольшие фрагменты данных вместо одного большого файла.
Данные запрашиваются только после определенного действия пользователя.
Информация поступает небольшими порциями.
Одним из наиболее распространенных подходов является запрос объектов внутри текущего viewport.
Mapbox GL JS предоставляет объект LngLatBounds.
const bounds = map.getBounds();
Получение координат:
const west = bounds.getWest();
const south = bounds.getSouth();
const east = bounds.getEast();
const north = bounds.getNorth();
Формирование запроса:
const bounds = map.getBounds();
const url =
`/api/objects?` +
`west=${bounds.getWest()}&` +
`south=${bounds.getSouth()}&` +
`east=${bounds.getEast()}&` +
`north=${bounds.getNorth()}`;
Для реализации lazy loading часто используется событие
moveend.
map.on('moveend', loadVisibleData);
Функция загрузки:
async function loadVisibleData() {
const bounds = map.getBounds();
const response = await fetch(
`/api/objects?` +
`west=${bounds.getWest()}&` +
`south=${bounds.getSouth()}&` +
`east=${bounds.getEast()}&` +
`north=${bounds.getNorth()}`
);
const geojson = await response.json();
map.getSource('objects').setData(geojson);
}
После завершения панорамирования карта получает только актуальные объекты.
Пользователь может быстро перемещать карту, вызывая множество запросов подряд.
Для борьбы с этим применяется debounce.
function debounce(callback, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
callback.apply(this, args);
}, delay);
};
}
Использование:
const debouncedLoad =
debounce(loadVisibleData, 300);
map.on('moveend', debouncedLoad);
Теперь запрос отправится только после завершения серии перемещений.
Во время быстрого перемещения пользователь может инициировать несколько запросов, часть которых уже потеряет актуальность.
Для решения используется AbortController.
let controller;
Перед каждым запросом:
async function loadVisibleData() {
if (controller) {
controller.abort();
}
controller = new AbortController();
const response = await fetch(
'/api/objects',
{
signal: controller.signal
}
);
const data = await response.json();
map.getSource('objects').setData(data);
}
Если появляется новый запрос, предыдущий немедленно отменяется.
Разные уровни детализации требуют разных объемов данных.
Например:
| Масштаб | Данные |
|---|---|
| 0–5 | Страны |
| 6–9 | Регионы |
| 10–13 | Города |
| 14+ | Отдельные объекты |
Получение текущего масштаба:
const zoom = map.getZoom();
Выбор источника:
async function loadData() {
const zoom = map.getZoom();
let endpoint;
if (zoom < 6) {
endpoint = '/api/countries';
}
else if (zoom < 10) {
endpoint = '/api/regions';
}
else {
endpoint = '/api/cities';
}
const response =
await fetch(endpoint);
const data =
await response.json();
map.getSource('layer')
.setData(data);
}
Запуск:
map.on('zoomend', loadData);
Наиболее эффективным способом ленивой загрузки являются vector tiles.
Mapbox GL JS изначально создавался для работы именно с ними.
Вместо загрузки большого GeoJSON карта получает небольшие тайлы.
Схема выглядит следующим образом:
Карта
↓
Видимая область
↓
Набор тайлов
↓
Загрузка только нужных тайлов
Пример подключения:
map.addSource('roads', {
type: 'vector',
tiles: [
'https://server.com/tiles/{z}/{x}/{y}.pbf'
]
});
При изменении положения карты автоматически загружаются только необходимые тайлы.
Система способна обслуживать миллионы объектов.
Передаются только данные для видимой области.
Формат PBF оптимизирован для работы в браузере.
Загруженные тайлы могут использоваться повторно.
Иногда не требуется получать подробную информацию обо всех объектах сразу.
Достаточно загрузить основные данные:
{
"id": 1001,
"name": "Office"
}
Подробности подгружаются после выбора объекта.
map.on('click', 'points', async (e) => {
const feature = e.features[0];
const response = await fetch(
`/api/details/${feature.id}`
);
const details =
await response.json();
console.log(details);
});
Такой подход существенно уменьшает начальный объем данных.
Некоторые тематические слои могут быть скрыты большую часть времени.
Вместо загрузки при старте приложения их создают по требованию.
Например, после нажатия кнопки.
button.addEventListener('click', async () => {
const response =
await fetch('/api/buildings');
const data =
await response.json();
map.addSource('buildings', {
type: 'geojson',
data: data
});
map.addLayer({
id: 'buildings',
type: 'fill',
source: 'buildings'
});
});
До момента открытия слоя никаких сетевых запросов не происходит.
При работе с собственными API часто применяется пространственное разбиение данных.
Карта делится на ячейки:
+----+----+----+
| A1 | A2 | A3 |
+----+----+----+
| B1 | B2 | B3 |
+----+----+----+
| C1 | C2 | C3 |
+----+----+----+
Каждая ячейка хранится отдельно.
Когда пользователь перемещает карту, вычисляются нужные клетки:
loadTile("A2");
loadTile("A3");
loadTile("B2");
loadTile("B3");
Этот подход напоминает работу настоящих картографических тайлов.
Повторный запрос одной и той же области не имеет смысла.
Простейший кэш:
const cache = new Map();
Проверка:
async function getData(tileId) {
if (cache.has(tileId)) {
return cache.get(tileId);
}
const response =
await fetch(`/tiles/${tileId}`);
const data =
await response.json();
cache.set(tileId, data);
return data;
}
Преимущества:
Для больших наборов точек эффективна комбинация двух технологий:
Источник:
map.addSource('earthquakes', {
type: 'geojson',
data: '/api/visible-data',
cluster: true,
clusterRadius: 50
});
Сначала загружается только видимая область, затем точки объединяются в кластеры.
Это позволяет отображать сотни тысяч объектов без критического падения производительности.
Для повышения плавности интерфейса используется предварительная загрузка соседних данных.
Схема:
Текущая область
↓
Загрузка
↓
Соседние области
Пользователь перемещается:
A → B
Данные области B уже находятся в памяти.
Подобный механизм применяется большинством картографических сервисов.
Событие idle возникает тогда, когда карта завершила все
текущие операции рендеринга.
map.on('idle', () => {
preloadNeighborTiles();
});
Это удобный момент для:
При длительной работе приложения кэш может бесконечно расти.
Обычно вводится ограничение.
const MAX_CACHE_SIZE = 100;
Удаление старых элементов:
if (cache.size > MAX_CACHE_SIZE) {
const firstKey =
cache.keys().next().value;
cache.delete(firstKey);
}
Так реализуется простейшая стратегия очистки памяти.
Если загруженные данные требуют дополнительной обработки, вычисления лучше выполнять в отдельном потоке.
Основной поток:
worker.postMessage(data);
Worker:
self.onmess age = function(event) {
const processed =
processData(event.data);
self.postMessage(processed);
};
Преимущества:
Типичная схема выглядит следующим образом:
Mapbox GL JS
↓
Viewport
↓
Bounding Box
↓
API
↓
База данных
↓
GeoJSON / Vector Tiles
↓
Кэш клиента
↓
Отображение
Каждый уровень отвечает за сокращение объема данных, передаваемых между сервером и браузером.
Для наборов до нескольких тысяч объектов
Для десятков тысяч объектов
Для сотен тысяч объектов и более
Для высоконагруженных геоинформационных систем