В интерактивных картографических приложениях многие события генерируются с чрезвычайно высокой частотой. Перемещение карты, масштабирование, вращение, изменение размеров окна браузера, ввод текста в поисковую строку — каждое из этих действий способно вызывать десятки и даже сотни событий в секунду.
Если на каждое такое событие выполнять сложные вычисления, сетевые запросы или обновление интерфейса, производительность приложения быстро ухудшается. Для решения этой проблемы применяется техника debouncing.
Debouncing — это механизм отложенного выполнения функции, при котором обработчик запускается только после того, как поток событий прекратился на заданный промежуток времени.
В контексте Mapbox GL JS debouncing особенно важен при работе с событиями карты, поскольку многие из них возникают непрерывно во время взаимодействия пользователя с картой.
Рассмотрим обработчик перемещения карты:
map.on('move', () => {
console.log(map.getCenter());
});
Событие move вызывается многократно во время
перетаскивания карты.
Если вместо простого вывода координат выполнять тяжёлые операции:
map.on('move', () => {
fetch('/api/search');
});
то при одном перемещении карты могут быть отправлены десятки запросов на сервер.
Подобная ситуация приводит к:
Пусть задержка составляет 500 миллисекунд.
Последовательность событий выглядит следующим образом:
Событие
|
|---100мс---|
|---100мс---|
|---100мс---|
|---100мс---|
|---100мс---|
Debounce Timer
перезапуск
перезапуск
перезапуск
перезапуск
перезапуск
500мс тишины
Выполнение функции
Каждое новое событие сбрасывает таймер.
Функция выполняется только тогда, когда события перестают поступать.
Классическая реализация:
function debounce(callback, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
callback.apply(this, args);
}, delay);
};
}
Разбор компонентов:
Хранит идентификатор таймера.
let timeoutId;
Удаляет ранее запущенный таймер.
clearTimeout(timeoutId);
Если пользователь продолжает взаимодействовать с картой, выполнение постоянно откладывается.
Создаёт новый таймер.
timeoutId = setTimeout(() => {
callback();
}, delay);
После завершения периода бездействия вызывается нужная функция.
Наиболее распространённый сценарий использования.
Без оптимизации:
map.on('move', () => {
updateMarkers();
});
С debouncing:
const debouncedUpdate = debounce(() => {
updateMarkers();
}, 300);
map.on('move', debouncedUpdate);
Теперь обновление маркеров произойдёт только после завершения перемещения карты.
Часто поисковая строка связана с геокодированием.
Без debouncing:
input.addEventListener('input', (event) => {
searchAddress(event.target.value);
});
При вводе слова из десяти символов будет выполнено десять запросов.
Оптимизированный вариант:
const debouncedSearch = debounce((query) => {
searchAddress(query);
}, 500);
input.addEventListener('input', (event) => {
debouncedSearch(event.target.value);
});
Теперь запрос выполняется только после завершения ввода.
Типичный сценарий:
map.on('move', () => {
const bounds = map.getBounds();
loadObjects(bounds);
});
Во время перемещения карты запросы будут отправляться непрерывно.
Лучшее решение:
const loadVisibleObjects = debounce(() => {
const bounds = map.getBounds();
loadObjects(bounds);
}, 400);
map.on('move', loadVisibleObjects);
Такой подход широко применяется в:
Событие масштабирования также генерируется очень часто.
Пример:
map.on('zoom', () => {
console.log(map.getZoom());
});
Оптимизированный вариант:
const debouncedZoomHandler = debounce(() => {
updateClusters();
}, 250);
map.on('zoom', debouncedZoomHandler);
После завершения масштабирования выполняется только одно обновление кластеров.
При вращении карты возникает аналогичная проблема.
const debouncedRotation = debounce(() => {
updateCompass();
}, 200);
map.on('rotate', debouncedRotation);
Обновление интерфейса выполняется значительно реже.
Событие изменения наклона карты:
const debouncedPitch = debounce(() => {
refreshBuildings();
}, 300);
map.on('pitch', debouncedPitch);
Особенно полезно при работе с трёхмерными слоями.
После изменения размеров окна нередко требуется пересчитать отображаемые данные.
Без оптимизации:
window.addEventListener('resize', () => {
map.resize();
});
Оптимизированный вариант:
const debouncedResize = debounce(() => {
map.resize();
}, 200);
window.addEventListener('resize', debouncedResize);
Современная реализация может выглядеть компактнее:
const debounce = (callback, delay) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
callback(...args);
}, delay);
};
};
Использование:
const debouncedMove = debounce(() => {
updateData();
}, 300);
map.on('move', debouncedMove);
Если используется метод класса, необходимо учитывать контекст объекта.
Пример класса:
class MapController {
constructor(map) {
this.map = map;
this.handleMove = debounce(
this.handleMove.bind(this),
300
);
map.on('move', this.handleMove);
}
handleMove() {
console.log(this.map.getCenter());
}
}
Метод bind() гарантирует корректную работу
this.
Очень часто debounce применяется совместно с асинхронными запросами.
const debouncedLoad = debounce(async () => {
const bounds = map.getBounds();
const response = await fetch('/api/objects');
const data = await response.json();
renderObjects(data);
}, 500);
map.on('move', debouncedLoad);
Пользователь может активно перемещать карту, но запрос будет выполнен только после остановки.
Получение координат по адресу — одна из самых распространённых задач.
const geocode = debounce(async (address) => {
const response = await fetch(
`/geocode?q=${address}`
);
const result = await response.json();
showResult(result);
}, 600);
Использование:
searchInput.addEventListener('input', (event) => {
geocode(event.target.value);
});
Подобный подход существенно снижает количество обращений к сервису геокодирования.
Подходящее значение зависит от задачи.
| Сценарий | Рекомендуемая задержка |
|---|---|
| Поиск адреса | 300–700 мс |
| Перемещение карты | 200–500 мс |
| Масштабирование | 150–400 мс |
| Изменение размеров окна | 100–300 мс |
| Сетевые запросы | 300–1000 мс |
Слишком маленькая задержка уменьшает эффект оптимизации.
Слишком большая задержка создаёт ощущение медленного интерфейса.
Mapbox GL JS предоставляет специальные события завершения действий.
Например:
map.on('moveend', () => {
updateData();
});
Во многих случаях это предпочтительнее debouncing.
Аналогично существуют:
map.on('zoomend', () => {
updateClusters();
});
map.on('rotateend', () => {
updateCompass();
});
Преимущества событий завершения:
Однако debouncing остаётся полезным, когда:
Иногда один обработчик должен реагировать на разные изменения карты.
const updateData = debounce(() => {
loadVisibleObjects();
}, 400);
map.on('move', updateData);
map.on('zoom', updateData);
map.on('rotate', updateData);
Даже если пользователь одновременно перемещает, масштабирует и вращает карту, данные обновятся только один раз.
При удалении карты желательно удалить подписку.
const debouncedHandler = debounce(() => {
updateData();
}, 300);
map.on('move', debouncedHandler);
map.off('move', debouncedHandler);
Важно использовать ту же ссылку на функцию.
Следующий вариант работать не будет:
map.off('move', debounce(updateData, 300));
Создаётся новый объект функции, который отличается от ранее зарегистрированного обработчика.
Неправильно:
map.on('move', () => {
debounce(updateData, 300)();
});
При каждом событии создаётся новый таймер.
Правильно:
const debouncedUpdate = debounce(updateData, 300);
map.on('move', debouncedUpdate);
const update = debounce(loadData, 5000);
Пять секунд ожидания делают интерфейс неотзывчивым.
Даже при использовании debounce запрос может оказаться устаревшим.
Например:
const controller = new AbortController();
fetch(url, {
signal: controller.signal
});
В сложных приложениях debouncing часто комбинируется с
AbortController, чтобы отменять старые запросы и
предотвращать обработку неактуальных данных.
function debounce(callback, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
callback(...args);
}, delay);
};
}
const loadMarkers = debounce(async () => {
const bounds = map.getBounds();
const response = await fetch(
`/api/markers?bbox=${bounds.toArray()}`
);
const markers = await response.json();
renderMarkers(markers);
}, 400);
map.on('move', loadMarkers);
map.on('zoom', loadMarkers);
Особенности данного решения:
Техника debouncing является одним из базовых инструментов оптимизации событийной модели в Mapbox GL JS. При грамотном использовании она позволяет эффективно контролировать частоту выполнения обработчиков, снижать нагрузку на клиентскую и серверную части приложения, а также поддерживать высокую отзывчивость интерфейса даже при интенсивном взаимодействии с картой.