В браузерных картах на основе Leaflet основная сложность производительности возникает не в рендеринге тайлов, а в обработке событий. Перемещения карты, зумирование, движение курсора и жесты вызывают десятки и сотни событий в секунду. Без ограничения частоты вызовов обработчиков возникает перегрузка JavaScript-потока, падение FPS и рост задержек интерфейса.
Внутренний цикл событий в Leaflet построен вокруг постоянного обновления состояния карты:
move — вызывается при каждом изменении центра
картыmousemove — при движении курсора по картеzoom — во время изменения масштабаzoomanim — во время анимации масштабированияmoveend, zoomend — финальные события после
завершения операцииПроблема заключается в том, что часть этих событий генерируется с частотой 30–120 раз в секунду. Если в обработчиках выполняются тяжёлые операции (запросы к серверу, фильтрация данных, перерасчёт слоёв), интерфейс начинает деградировать.
Типичный антипример:
map.on('move', () => {
updateMarkers();
fetchVisibleObjects();
renderSidebar();
});
Здесь каждая микродвижение карты запускает полный цикл бизнес-логики. Даже при слабой нагрузке это приводит к:
Для решения используются два базовых механизма: throttling и debouncing.
Throttling (троттлинг) — это стратегия, при которой функция выполняется не чаще заданного интервала времени, независимо от количества вызовов.
Идея: «выполнять не каждый раз, а раз в N миллисекунд».
function throttle(fn, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
fn.apply(this, args);
}
};
}
Применение в Leaflet:
const throttledMoveHandler = throttle((e) => {
console.log('центр карты:', e.target.getCenter());
}, 200);
map.on('move', throttledMoveHandler);
При панорамировании:
move продолжают генерироватьсяФункция выполняется в начале интервала:
function throttleLeading(fn, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
fn.apply(this, args);
}
};
}
Полезен при обработке итогового состояния карты.
Debouncing (дебаунсинг) — стратегия, при которой функция выполняется только после того, как поток событий остановился на заданное время.
Идея: «выполнить только когда пользователь перестал двигать карту».
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
const debouncedMoveEnd = debounce((e) => {
const center = e.target.getCenter();
console.log('финальный центр:', center);
}, 300);
map.on('move', debouncedMoveEnd);
Хотя чаще debounce применяют не к move, а к:
map.on('moveend', (e) => {
console.log('карта остановилась');
});
Но debounce полезен, когда требуется задержка после активности:
В Leaflet выбор стратегии зависит от поведения UI:
Примеры:
Примеры:
Для высокочастотных событий в Leaflet часто эффективнее throttling
через requestAnimationFrame.
Синхронизация с циклом перерисовки браузера (~60 FPS).
function rafThrottle(fn) {
let running = false;
return function (...args) {
if (running) return;
running = true;
requestAnimationFrame(() => {
fn.apply(this, args);
running = false;
});
};
}
map.on('move', rafThrottle((e) => {
updateOverlay(e.target.getCenter());
}));
Одно из самых нагруженных мест в Leaflet — подгрузка данных при перемещении карты.
map.on('move', () => {
const bounds = map.getBounds();
loadObjects(bounds);
});
Это приведёт к десяткам запросов в секунду.
const loadObjectsDebounced = debounce(() => {
const bounds = map.getBounds();
loadObjects(bounds);
}, 400);
map.on('move', loadObjectsDebounced);
Результат:
const loadObjectsThrottled = throttle(() => {
const bounds = map.getBounds();
loadObjects(bounds);
}, 500);
map.on('move', loadObjectsThrottled);
Результат:
В реальных приложениях Leaflet часто используется гибрид:
throttle для визуальных обновленийdebounce для серверных запросовmap.on('move', throttle(updateUI, 100));
map.on('move', debounce(fetchData, 300));
Так достигается баланс между отзывчивостью и экономией ресурсов.
map.on('move', debounce(handler, 200));
map.on('move', debounce(handler, 200));
Каждый вызов создаёт новый debounce-объект → утечка логики.
function handler() {
this.getCenter(); // может быть undefined
}
Решение — использовать fn.apply(this, args) или
стрелочные функции.
Часто логика дублируется:
movemoveendЭто приводит к двойным запросам.
На мобильных устройствах в Leaflet частота событий выше из-за инерции жестов.
Рекомендуется:
moveПри обновлении маркеров в зависимости от viewport:
const updateMarkersDebounced = debounce(() => {
const bounds = map.getBounds();
const visible = allMarkers.filter(m => bounds.contains(m.getLatLng()));
renderMarkers(visible);
}, 250);
map.on('zoom move', updateMarkersDebounced);
Это предотвращает повторный пересчёт при каждом пиксельном сдвиге.
В сложных приложениях на Leaflet вводится промежуточный слой:
const viewportChanged = debounce(() => {
eventBus.emit('viewportChanged', map.getBounds());
}, 300);
map.on('move', viewportChanged);
map.on('zoom', viewportChanged);
Это снижает связанность компонентов и упрощает масштабирование логики.