Работа с картой в браузере связана с большим количеством высокочастотных событий: перемещение карты, изменение масштаба, перетаскивание, движение мыши, изменение границ видимой области. Многие из этих событий могут вызываться десятки и сотни раз в секунду, что приводит к избыточным вычислениям, сетевым запросам и деградации производительности интерфейса.
Для управления этим потоком используется два фундаментальных приёма: debouncing и throttling.
В API карт особенно часто используются следующие события:
bounds_changedcenter_changedzoom_changeddragmousemoveidleКаждое из них может генерироваться с высокой частотой при одном
действии пользователя. Например, при перетаскивании карты событие
bounds_changed может вызываться десятки раз в секунду, а
mousemove — ещё чаще.
Типичная проблема: привязка логики загрузки данных напрямую к этим событиям приводит к лавинообразному числу запросов.
Debouncing объединяет серию вызовов в один: функция выполняется только после того, как события перестали поступать в течение заданного времени.
Throttling ограничивает частоту вызовов: функция выполняется не чаще одного раза в заданный интервал времени.
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
Принцип: каждый новый вызов сбрасывает таймер. Реальное выполнение происходит только после паузы в активности.
function throttle(fn, limit) {
let inThrottle = false;
return function (...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
Принцип: выполнение допускается один раз за интервал времени, остальные вызовы игнорируются до завершения периода.
Одна из самых распространённых задач — загрузка объектов в пределах видимой области карты.
Без оптимизации:
map.addListener("bounds_changed", () => {
loadMarkers();
});
Проблема: loadMarkers() вызывается слишком часто.
С debouncing:
const debouncedLoadMarkers = debounce(() => {
const bounds = map.getBounds();
if (!bounds) return;
loadMarkers(bounds);
}, 400);
map.addListener("bounds_changed", debouncedLoadMarkers);
Здесь запрос выполняется только после того, как пользователь завершил перемещение карты.
Событие idle срабатывает, когда карта завершает любые
изменения и становится «стабильной».
map.addListener("idle", () => {
const bounds = map.getBounds();
loadMarkers(bounds);
});
Во многих сценариях idle заменяет необходимость
debounce, но не всегда подходит при сложных интерактивных сценариях.
При работе с пользовательскими слоями или визуализацией координат курсора важно ограничивать частоту обновлений.
const throttledMouseMove = throttle((event) => {
const lat = event.latLng.lat();
const lng = event.latLng.lng();
updateOverlay(lat, lng);
}, 50);
map.addListener("mousemove", throttledMouseMove);
Без throttling событие mousemove создаёт избыточную
нагрузку на DOM и рендеринг.
Debouncing особенно полезен в следующих задачах:
Пример с вводом:
const input = document.getElementById("search");
const debouncedSearch = debounce((value) => {
geocodeAddress(value);
}, 300);
input.addEventListener("input", (e) => {
debouncedSearch(e.target.value);
});
Throttling применяется там, где важно сохранить регулярность обновлений:
При работе с API часто используется комбинация debouncing и AbortController для отмены устаревших запросов.
let controller;
const debouncedFetch = debounce(async (bounds) => {
if (controller) controller.abort();
controller = new AbortController();
const res = await fetch("/api/markers", {
signal: controller.signal,
method: "POST",
body: JSON.stringify(bounds),
});
const data = await res.json();
renderMarkers(data);
}, 400);
Так исключается ситуация, когда старые запросы перекрывают новые результаты.
Для визуальных обновлений часто используется
requestAnimationFrame, синхронизированный с рендером
браузера.
let scheduled = false;
map.addListener("mousemove", (event) => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
const latLng = event.latLng;
drawPointer(latLng);
});
});
Этот подход снижает нагрузку и делает обновления плавными.
События изменения масштаба и границ требуют особого внимания, поскольку часто вызываются совместно.
Типичная ошибка — дублирование логики:
map.addListener("zoom_changed", update);
map.addListener("bounds_changed", update);
В результате update() вызывается несколько раз за одно
действие пользователя.
Решение — объединение через debounce:
const updateView = debounce(() => {
const bounds = map.getBounds();
const zoom = map.getZoom();
refreshData(bounds, zoom);
}, 300);
map.addListener("zoom_changed", updateView);
map.addListener("bounds_changed", updateView);
this при передаче методов объектаremoveListenerОчистка слушателей:
const listener = map.addListener("idle", handler);
// позже
listener.remove();
При проектировании логики взаимодействия с картой обычно применяется комбинация подходов:
idle для финального состоянияТак достигается баланс между отзывчивостью интерфейса и стабильной производительностью без избыточных вычислений.