Карты в Leaflet строятся вокруг асинхронной загрузки тайлов, векторных данных и дополнительных слоёв. Основной источник задержек — сетевые запросы к тайловым серверам. При этом библиотека предоставляет событийную модель, позволяющую отслеживать состояние загрузки на уровне карты и отдельных слоёв.
Ключевые особенности:
Это создаёт основу для построения собственных индикаторов.
Leaflet предоставляет два базовых события уровня карты:
loading — начинается процесс загрузки хотя бы одного
слоя;load — все активные загрузки завершены.map.on('loading', () => {
console.log('Начало загрузки данных карты');
});
map.on('load', () => {
console.log('Все данные загружены');
});
Особенность этих событий заключается в том, что они агрегируют
состояние всех подключённых слоёв, включая TileLayer,
ImageOverlay, GeoJSON и другие асинхронные
источники.
Наиболее важный источник индикаторов — TileLayer.
Основные события:
loading — старт загрузки тайлов;load — завершение загрузки всех тайлов текущего
видимого набора;tileloadstart — начало загрузки отдельного тайла;tileload — успешная загрузка тайла;tileerror — ошибка загрузки тайла.const tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png');
tileLayer.on('loading', () => {
console.log('Загрузка тайлов началась');
});
tileLayer.on('tileload', (e) => {
console.log('Тайл загружен:', e.tile.src);
});
tileLayer.on('tileerror', (e) => {
console.log('Ошибка загрузки тайла:', e.tile.src);
});
tileLayer.on('load', () => {
console.log('Все тайлы загружены');
});
Эти события позволяют строить как простые индикаторы активности, так и детализированные прогресс-бары.
Самый распространённый подход — отображение слоя-перекрытия с индикатором.
<div id="map"></div>
<div id="loader" class="loader hidden"></div>
#map {
height: 100vh;
}
.loader {
position: absolute;
top: 50%;
left: 50%;
width: 40px;
height: 40px;
border: 4px solid #ccc;
border-top-color: #1a73e8;
border-radius: 50%;
animation: spin 1s linear infinite;
transform: translate(-50%, -50%);
z-index: 1000;
}
.hidden {
display: none;
}
@keyframes spin {
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
const loader = document.getElementById('loader');
function showLoader() {
loader.classList.remove('hidden');
}
function hideLoader() {
loader.classList.add('hidden');
}
map.on('loading', showLoader);
map.on('load', hideLoader);
Такой подход отражает только общее состояние загрузки без детализации.
Для более точного контроля используется счётчик активных загрузок.
let activeTiles = 0;
function updateLoader() {
if (activeTiles > 0) {
showLoader();
} else {
hideLoader();
}
}
tileLayer.on('tileloadstart', () => {
activeTiles++;
updateLoader();
});
tileLayer.on('tileload', () => {
activeTiles--;
updateLoader();
});
tileLayer.on('tileerror', () => {
activeTiles--;
updateLoader();
});
Такой механизм позволяет учитывать реальные сетевые операции, а не только агрегированные события слоя.
Leaflet не предоставляет процент загрузки, но его можно вычислить относительно ожидаемого количества тайлов.
let totalTiles = 0;
let loadedTiles = 0;
function updateProgress() {
const percent = totalTiles === 0 ? 0 : (loadedTiles / totalTiles) * 100;
console.log(`Загрузка: ${percent.toFixed(0)}%`);
}
tileLayer.on('loading', () => {
totalTiles = 0;
loadedTiles = 0;
});
tileLayer.on('tileloadstart', () => {
totalTiles++;
});
tileLayer.on('tileload', () => {
loadedTiles++;
updateProgress();
});
tileLayer.on('tileerror', () => {
loadedTiles++;
updateProgress();
});
Такой подход не идеально точен из-за динамической подгрузки при перемещении карты, но даёт приемлемую визуальную оценку.
Каждый слой может иметь собственную логику отображения состояния.
function attachLayerLoader(layer) {
layer.on('loading', () => {
console.log('Слой загружается');
});
layer.on('load', () => {
console.log('Слой загружен');
});
}
Это особенно важно при работе с несколькими слоями одновременно:
Leaflet позволяет создавать собственные UI-компоненты через
L.Control.
const LoadingControl = L.Control.extend({
onAdd: function () {
const div = L.DomUtil.create('div', 'loading-control');
div.innerHTML = '0%';
return div;
}
});
const loadingControl = new LoadingControl({ position: 'topright' });
map.addControl(loadingControl);
Обновление состояния:
function setLoadingText(text) {
const el = document.querySelector('.loading-control');
if (el) el.innerHTML = text;
}
При наличии нескольких TileLayer или комбинированных
источников требуется централизованный трекинг.
let loadingSources = 0;
function startLoading() {
loadingSources++;
showLoader();
}
function endLoading() {
loadingSources--;
if (loadingSources <= 0) {
loadingSources = 0;
hideLoader();
}
}
function attachTracking(layer) {
layer.on('loading', startLoading);
layer.on('load', endLoading);
}
attachTracking(tileLayer1);
attachTracking(tileLayer2);
Такой подход предотвращает преждевременное скрытие индикатора при параллельных загрузках.
Ошибки тайлов напрямую влияют на UX и должны учитываться в логике индикатора.
tileLayer.on('tileerror', (e) => {
console.warn('Ошибка тайла:', e.tile.src);
});
Расширенный вариант с повторными попытками:
tileLayer.on('tileerror', (e) => {
const img = e.tile;
setTimeout(() => {
img.src = img.src;
}, 1000);
});
При массовой загрузке тайлов события могут генерироваться слишком
часто. Для снижения нагрузки применяется
requestAnimationFrame.
let pendingUpdate = false;
function scheduleUpdate() {
if (!pendingUpdate) {
pendingUpdate = true;
requestAnimationFrame(() => {
updateProgress();
pendingUpdate = false;
});
}
}
Использование такого подхода снижает количество DOM-операций при интенсивной загрузке.
На практике индикаторы объединяют несколько сигналов:
loading/load);map.on('loading', showLoader);
map.on('load', hideLoader);
tileLayer.on('tileloadstart', () => {
activeTiles++;
updateLoader();
});
tileLayer.on('tileload', () => {
activeTiles--;
updateLoader();
});
Такая комбинация обеспечивает согласованное отображение состояния независимо от источника данных.
Перемещение и масштабирование карты вызывает повторную загрузку
тайлов. Это приводит к частым переходам состояния
loading → load.
Особенность:
При добавлении и удалении слоёв важно учитывать их жизненный цикл.
map.addLayer(tileLayer);
tileLayer.on('load', () => {
console.log('Динамический слой загружен');
});
map.removeLayer(tileLayer);
Без корректного отслеживания возможно «зависание» индикатора из-за незавершённых событий.