Ленивая загрузка (Lazy Loading) — подход к оптимизации веб-карт, при котором данные, стили, изображения и дополнительные компоненты загружаются только в момент фактической необходимости. Вместо одновременной загрузки всех ресурсов при инициализации карты приложение постепенно подгружает содержимое по мере взаимодействия пользователя с интерфейсом.
Для проектов на Mapbox GL JS этот подход особенно важен, поскольку карты могут содержать:
Ленивая загрузка позволяет уменьшить объём первоначально загружаемых данных, сократить время отображения интерфейса и снизить нагрузку на сеть.
Грамотно реализованная ленивая загрузка решает несколько задач одновременно:
Ускорение первоначального рендеринга
Карта становится доступной пользователю быстрее, поскольку загружается только минимально необходимый набор данных.
Снижение потребления памяти
В браузере хранятся только используемые ресурсы.
Экономия сетевого трафика
Неиспользуемые данные не передаются клиенту заранее.
Повышение отзывчивости интерфейса
Большие наборы геоданных не блокируют работу приложения во время запуска.
Mapbox GL JS уже использует ленивую загрузку на уровне тайлов.
При перемещении карты движок самостоятельно:
Упрощённая схема работы выглядит следующим образом:
Пользователь перемещает карту
↓
Вычисляется новый viewport
↓
Определяются необходимые тайлы
↓
Загружаются отсутствующие тайлы
↓
Выполняется отрисовка
Этот механизм встроен в библиотеку и не требует дополнительной настройки.
Одной из наиболее распространённых задач является отложенная загрузка больших GeoJSON-файлов.
Плохой вариант:
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12'
});
map.on('load', () => {
map.addSource('buildings', {
type: 'geojson',
data: '/data/buildings.geojson'
});
});
В данном случае файл загружается сразу после инициализации карты независимо от того, понадобится он или нет.
Более эффективный подход:
let buildingsLoaded = false;
map.on('moveend', async () => {
if (buildingsLoaded) {
return;
}
const zoom = map.getZoom();
if (zoom >= 14) {
const response = await fetch('/data/buildings.geojson');
const data = await response.json();
map.addSource('buildings', {
type: 'geojson',
data
});
map.addLayer({
id: 'buildings',
type: 'fill',
source: 'buildings',
paint: {
'fill-color': '#4a90e2'
}
});
buildingsLoaded = true;
}
});
Теперь данные загружаются только при достижении необходимого масштаба.
Крупные геоинформационные системы часто используют различные наборы данных для разных масштабов.
Например:
| Масштаб | Данные |
|---|---|
| 0–5 | Страны |
| 6–9 | Регионы |
| 10–13 | Города |
| 14+ | Здания |
Реализация:
let cityLayerLoaded = false;
map.on('zoomend', async () => {
const zoom = map.getZoom();
if (zoom >= 10 && !cityLayerLoaded) {
const response = await fetch('/api/cities');
const cities = await response.json();
map.addSource('cities', {
type: 'geojson',
data: cities
});
cityLayerLoaded = true;
}
});
Такой подход существенно уменьшает объём данных, необходимых на малых масштабах.
Не все слои должны существовать в карте с момента запуска.
Допустим, слой дорожных камер отображается только после включения соответствующего переключателя.
document
.getElementById('show-cameras')
.addEventListener('click', async () => {
if (map.getSource('cameras')) {
return;
}
const response = await fetch('/api/cameras');
const data = await response.json();
map.addSource('cameras', {
type: 'geojson',
data
});
map.addLayer({
id: 'cameras',
type: 'circle',
source: 'cameras'
});
});
Пока пользователь не запросит отображение камер, данные не будут загружаться.
Mapbox GL JS позволяет использовать пользовательские иконки.
Без оптимизации:
map.loadImage('/icons/airport.png', (error, image) => {
if (error) throw error;
map.addImage('airport', image);
});
Если десятки иконок загружаются одновременно, это увеличивает время запуска.
Более эффективный вариант:
async function loadAirportIcon() {
if (map.hasImage('airport')) {
return;
}
const image = await map.loadImage('/icons/airport.png');
map.addImage('airport', image.data);
}
Загрузка выполняется только в момент появления соответствующего слоя.
Событие idle возникает тогда, когда карта завершила все
текущие операции рендеринга и загрузки.
Это удобная точка для фоновой подгрузки данных.
map.on('idle', async () => {
if (cacheLoaded) {
return;
}
const response = await fetch('/api/preload');
cache = await response.json();
cacheLoaded = true;
});
Такой подход не влияет на скорость первоначального отображения карты.
Очень часто сервер предоставляет данные только для видимой области.
Получение текущих границ:
const bounds = map.getBounds();
Запрос:
async function loadVisibleData() {
const bounds = map.getBounds();
const response = await fetch(
`/api/objects?bbox=${
bounds.getWest()
},${
bounds.getSouth()
},${
bounds.getEast()
},${
bounds.getNorth()
}`
);
return response.json();
}
Преимущества:
Наиболее распространённый механизм основан на событии
moveend.
map.on('moveend', async () => {
const data = await loadVisibleData();
map.getSource('objects').setData(data);
});
Загрузка выполняется только после завершения движения карты.
Это лучше, чем использование события move, которое
вызывается десятки раз в секунду.
При активной работе с картой возможно возникновение большого количества сетевых запросов.
Для ограничения используется debounce.
function debounce(callback, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
callback(...args);
}, delay);
};
}
Применение:
const upd ateData = debounce(async () => {
const data = await loadVisibleData();
map.getSource('objects').setData(data);
}, 500);
map.on('moveend', updateData);
Сервер будет получать значительно меньше запросов.
Современный JavaScript позволяет загружать код по требованию.
Например, модуль тепловой карты нужен только после выбора определённого режима отображения.
button.addEventListener('click', async () => {
const heatmapModule =
await import('./heatmap.js');
heatmapModule.initialize(map);
});
Преимущества:
Сложные элементы интерфейса также могут подключаться по запросу.
async function loadMeasurementTool() {
const module =
await import('./measurement-tool.js');
const control =
new module.MeasurementControl();
map.addControl(control);
}
Пока инструмент измерения не используется, его код отсутствует в памяти браузера.
Ленивая загрузка может сочетаться с предзагрузкой.
Сначала отображается только необходимый набор данных:
loadCurrentViewport();
После завершения работы карты выполняется фоновая подкачка соседних областей:
map.on('idle', preloadNearbyAreas);
Подобная стратегия позволяет добиться ощущения мгновенной работы при последующих перемещениях карты.
Без кэширования одни и те же данные могут запрашиваться многократно.
Простейший вариант:
const cache = new Map();
Получение данных:
async function getData(url) {
if (cache.has(url)) {
return cache.get(url);
}
const response = await fetch(url);
const data = await response.json();
cache.se t(url, data);
return data;
}
Кэш значительно снижает нагрузку на сервер и сеть.
Ленивая загрузка должна сопровождаться освобождением памяти.
Удаление слоя:
if (map.getLayer('traffic')) {
map.removeLayer('traffic');
}
Удаление источника:
if (map.getSource('traffic')) {
map.removeSource('traffic');
}
Это особенно важно для длительно работающих веб-приложений.
При быстром перемещении карты старые запросы могут становиться неактуальными.
Для отмены используется AbortController.
let controller;
Перед выполнением нового запроса:
if (controller) {
controller.abort();
}
controller = new AbortController();
const response = await fetch(url, {
signal: controller.signal
});
В результате обрабатываются только актуальные данные.
В крупных проектах обычно применяется несколько механизмов одновременно:
Пример общего сценария:
Инициализация карты
↓
Загрузка базового стиля
↓
Отображение интерфейса
↓
Пользователь меняет масштаб
↓
Подгружаются необходимые слои
↓
Пользователь перемещает карту
↓
Запрашиваются данные текущего bbox
↓
Результаты помещаются в кэш
↓
В фоне загружаются соседние области
Загрузка всех данных при старте
fetch('/all-data.geojson');
Такой подход плохо масштабируется.
Использование события move
map.on('move', loadData);
Может создавать десятки запросов каждую секунду.
Отсутствие кэша
Повторная загрузка уже полученных данных увеличивает сетевой трафик.
Отсутствие отмены запросов
Устаревшие ответы способны перезаписывать актуальные данные.
Избыточная предзагрузка
Слишком агрессивная фоновая загрузка сводит преимущества ленивого подхода к минимуму.
Для небольших проектов
moveend;Для средних проектов
AbortController;Для крупных геоинформационных систем
Грамотно реализованная ленивая загрузка превращает карту из тяжёлого монолитного приложения в адаптивную систему, которая потребляет ресурсы только в момент реальной необходимости, обеспечивая высокую производительность даже при работе с большими объёмами географических данных.