При разработке интерактивных карт на базе Mapbox GL JS большое количество пользовательских событий способно создавать чрезмерную нагрузку на приложение. Перемещение карты, масштабирование колесом мыши, изменение размеров окна браузера, работа с поиском по карте, обновление данных в реальном времени — все эти действия могут генерировать десятки и сотни событий в секунду.
Если каждое событие приводит к выполнению ресурсоёмкой логики, возникают следующие проблемы:
Для решения подобных задач применяется техника throttling.
Throttling — это ограничение частоты выполнения функции. Независимо от количества вызовов функция будет запускаться не чаще заданного интервала времени.
Например, при ограничении в 200 миллисекунд:
Эти техники часто используются совместно, однако решают разные задачи.
Выполняет функцию через равные промежутки времени.
Схема работы:
События:
|||||||||||||||||||||||||||||
Вызовы:
|----|----|----|----|----|
Подходит для:
Откладывает выполнение до окончания серии событий.
Схема работы:
События:
|||||||||||||||||||||||||||||
Вызов:
|
Подходит для:
Классическая реализация строится на основе временной метки последнего вызова.
function throttle(callback, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
callback.apply(this, args);
}
};
}
Использование:
const logPosition = throttle(() => {
console.log("Карта перемещается");
}, 300);
Событие move является одним из самых часто вызываемых
событий Mapbox GL JS.
Оно срабатывает:
Без ограничений обработчик может выполняться десятки раз за секунду.
map.on("move", () => {
const center = map.getCenter();
console.log(center.lng, center.lat);
});
Во время активного перемещения карта будет постоянно выполнять обработчик.
const updateCoordinates = throttle(() => {
const center = map.getCenter();
console.log(center.lng, center.lat);
}, 200);
map.on("move", updateCoordinates);
Теперь координаты обновляются не чаще одного раза каждые 200 миллисекунд.
Часто требуется загружать объекты только для текущей области просмотра.
Например:
map.on("move", async () => {
const bounds = map.getBounds();
const response = await fetch(
`/api/objects?bbox=${bounds.toArray()}`
);
const data = await response.json();
updateMarkers(data);
});
Подобный код может генерировать огромное количество сетевых запросов.
const loadObjects = throttle(async () => {
const bounds = map.getBounds();
const response = await fetch(
`/api/objects?bbox=${bounds.toArray()}`
);
const data = await response.json();
updateMarkers(data);
}, 1000);
map.on("move", loadObjects);
Теперь запросы выполняются максимум раз в секунду.
Событие zoom также способно вызывать обработчики очень
часто.
Например, при обновлении пользовательского интерфейса:
map.on("zoom", () => {
document.getElementById("zoom-value").textContent =
map.getZoom().toFixed(2);
});
После оптимизации:
const updateZoom = throttle(() => {
document.getElementById("zoom-value").textContent =
map.getZoom().toFixed(2);
}, 100);
map.on("zoom", updateZoom);
На крупных проектах источники данных могут содержать десятки тысяч объектов.
Например:
map.on("move", () => {
source.setData(generateGeoJSON());
});
Постоянное обновление GeoJSON приводит к:
Более безопасный вариант:
const updateSource = throttle(() => {
source.setData(generateGeoJSON());
}, 500);
map.on("move", updateSource);
Для визуальных операций часто предпочтительнее использовать связку
throttling и requestAnimationFrame.
function throttleAnimation(callback) {
let scheduled = false;
return function (...args) {
if (!scheduled) {
scheduled = true;
requestAnimationFrame(() => {
callback.apply(this, args);
scheduled = false;
});
}
};
}
Использование:
const updateOverlay = throttleAnimation(() => {
drawOverlay();
});
map.on("move", updateOverlay);
Такой подход синхронизирует обновления с циклом отрисовки браузера.
Событие mousemove является одним из самых
нагруженных.
Пример без оптимизации:
map.on("mousemove", (e) => {
showCoordinates(
e.lngLat.lng,
e.lngLat.lat
);
});
Во время движения мыши генерируется огромное количество событий.
Оптимизированная версия:
const updateMousePosition = throttle((e) => {
showCoordinates(
e.lngLat.lng,
e.lngLat.lat
);
}, 100);
map.on("mousemove", updateMousePosition);
Во многих проектах throttling реализуется через библиотеку Lodash.
Установка:
npm install lodash
Импорт:
import throttle from "lodash/throttle";
Пример:
const updateData = throttle(() => {
console.log("Обновление");
}, 500);
map.on("move", updateData);
Преимущества:
Lodash предоставляет гибкие параметры.
const handler = throttle(
callback,
500,
{
leading: true,
trailing: false
}
);
Поведение:
События:
||||||||||||||||||||
Вызов:
|
const handler = throttle(
callback,
500,
{
leading: false,
trailing: true
}
);
Поведение:
События:
||||||||||||||||||||
Вызов:
|
const handler = throttle(
callback,
500,
{
leading: true,
trailing: true
}
);
Поведение:
События:
||||||||||||||||||||
Вызовы:
| |
Lodash позволяет отменять ожидающий вызов.
const updateData = throttle(
loadData,
1000
);
Отмена:
updateData.cancel();
Особенно полезно при уничтожении карты.
map.remove();
updateData.cancel();
При работе с одностраничными приложениями необходимо удалять обработчики событий.
const moveHandler = throttle(() => {
updateSidebar();
}, 300);
map.on("move", moveHandler);
Удаление:
map.off("move", moveHandler);
Для приложений на React, Vue или Angular это особенно важно, поскольку предотвращает утечки памяти.
Во время перемещения карты может выполняться перерасчёт кластеров.
Пример:
map.on("move", () => {
recalculateClusters();
});
На больших объёмах данных такая операция становится дорогой.
Оптимизация:
const clusterUpdate = throttle(() => {
recalculateClusters();
}, 500);
map.on("move", clusterUpdate);
Частая задача — отображение объектов, находящихся в текущем окне карты.
Без ограничений:
map.on("move", () => {
renderVisibleObjects();
});
После оптимизации:
const updateSidebar = throttle(() => {
renderVisibleObjects();
}, 300);
map.on("move", updateSidebar);
Это особенно важно при работе с тысячами записей.
На практике часто используется гибридный подход.
Во время движения карта получает лёгкие обновления:
const previewUpdate = throttle(() => {
updatePreview();
}, 300);
map.on("move", previewUpdate);
После завершения движения выполняется полная операция:
map.on("moveend", () => {
loadFullDataset();
});
Преимущества подхода:
Рекомендуемые значения для различных задач:
| Задача | Интервал |
|---|---|
| Отображение координат курсора | 50–100 мс |
| Обновление интерфейса | 100–200 мс |
| Перемещение карты | 200–300 мс |
| Загрузка данных | 500–1000 мс |
| Тяжёлые вычисления | 1000–3000 мс |
Слишком маленький интервал практически не даёт выигрыша по производительности.
Слишком большой интервал делает интерфейс менее отзывчивым.
Неправильно:
map.on("move", () => {
const handler = throttle(() => {
updateData();
}, 300);
handler();
});
Каждый вызов создаёт новый экземпляр throttling.
Правильно:
const handler = throttle(() => {
updateData();
}, 300);
map.on("move", handler);
const updateData = throttle(
refresh,
10000
);
Интерфейс будет обновляться раз в десять секунд, что создаёт ощущение зависания приложения.
const updateData = throttle(
loadData,
1000
);
Если приложение уничтожает карту, но throttled-функция остаётся активной, возможно выполнение лишнего кода после удаления компонентов.
Throttling следует использовать для всех событий высокой частоты в Mapbox GL JS:
move;mousemove;zoom;drag;rotate;pitch;resize.Особенно полезен throttling для:
Грамотно подобранный throttling позволяет сохранять плавность навигации по карте, снижать нагрузку на браузер и серверную инфраструктуру, а также обеспечивать стабильную работу приложений с большими наборами пространственных данных.