При работе с картографическими интерфейсами количество пользовательских событий может быть чрезвычайно высоким. Перемещение карты, изменение масштаба, ввод текста в фильтрах, переключение слоёв, выбор объектов и другие действия способны генерировать десятки или сотни событий в секунду.
В экосистеме Kepler.gl подобное поведение особенно заметно при работе с крупными наборами геоданных. Каждое изменение состояния может инициировать:
Без ограничения частоты обработки событий приложение начинает выполнять множество избыточных операций, что приводит к:
Для решения подобных проблем применяется дебаунсинг.
Debounce (дебаунсинг) — техника, позволяющая откладывать выполнение функции до тех пор, пока не пройдёт определённое время после последнего вызова.
Если событие возникает повторно до истечения заданного интервала, таймер сбрасывается и отсчёт начинается заново.
Предположим, пользователь вводит текст:
М
Мо
Моск
Москва
Без дебаунсинга обработчик выполнится четыре раза.
С дебаунсингом в 500 мс функция выполнится только один раз — после завершения ввода.
Схема работы:
Событие
↓
Запуск таймера
↓
Новое событие?
↓ Да
Сброс таймера
↓
Ожидание
↓ Нет
Выполнение функции
Эти механизмы часто путают.
Выполняет функцию после прекращения серии событий.
События:
||||||||||||||||
Вызов:
X
Подходит для:
Выполняет функцию не чаще заданного интервала.
События:
||||||||||||||||
Вызовы:
X----X----X----X
Подходит для:
В приложениях с Kepler.gl часто используются оба подхода одновременно.
Базовая версия:
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
Использование:
const updateData = debounce(() => {
console.log("Обновление данных");
}, 500);
updateData();
updateData();
updateData();
После серии вызовов функция выполнится только один раз.
Фильтрация — один из самых распространённых сценариев.
Рассмотрим поиск объектов по названию.
Без дебаунсинга:
input.addEventListener("input", event => {
performSearch(event.target.value);
});
Каждое нажатие клавиши запускает поиск.
Для строки из десяти символов будет выполнено десять операций.
Более эффективный вариант:
const debouncedSearch = debounce(value => {
performSearch(value);
}, 400);
input.addEventListener("input", event => {
debouncedSearch(event.target.value);
});
Теперь поиск выполняется только после завершения ввода.
Kepler.gl часто используется для отображения больших объёмов геоданных:
Предположим, пользователь изменяет параметры выборки.
Без дебаунсинга:
function onFilterChange(filters) {
loadData(filters);
}
Каждое изменение запускает новый запрос.
С дебаунсингом:
const loadDataDebounced = debounce(filters => {
loadData(filters);
}, 800);
function onFilterChange(filters) {
loadDataDebounced(filters);
}
В результате выполняется только последний актуальный запрос.
При интеграции Kepler.gl с внешним API проблема становится особенно заметной.
Например:
function fetchGeoData(query) {
return fetch(`/api/geo?q=${query}`);
}
Без ограничения пользователь может инициировать десятки запросов за несколько секунд.
Решение:
const debouncedFetch = debounce(query => {
fetchGeoData(query);
}, 500);
Использование:
searchInput.addEventListener("input", e => {
debouncedFetch(e.target.value);
});
Нагрузка на сервер существенно уменьшается.
В реальных проектах чаще применяется готовая реализация из Lodash.
Установка:
npm install lodash
Импорт:
import debounce from "lodash/debounce";
Пример:
const updateFilters = debounce(() => {
console.log("Фильтры обновлены");
}, 300);
Преимущества:
По умолчанию debounce выполняет функцию после завершения серии событий.
Иногда требуется выполнить функцию сразу.
Для этого используется параметр leading.
const handler = debounce(
() => {
console.log("Выполнено");
},
1000,
{
leading: true,
trailing: false
}
);
Поведение:
Клик
↓
Функция выполняется немедленно
↓
Последующие клики игнорируются
↓
Истечение интервала
Подобный режим удобен для защиты кнопок от повторного нажатия.
Отвечает за выполнение после завершения серии вызовов.
Стандартное поведение:
const handler = debounce(
callback,
500,
{
trailing: true
}
);
Именно этот вариант чаще всего используется при работе с Kepler.gl.
Возможно одновременное использование обоих режимов.
const handler = debounce(
callback,
1000,
{
leading: true,
trailing: true
}
);
В таком случае:
Подход полезен для отображения промежуточных результатов при длительных пользовательских взаимодействиях.
Lodash предоставляет метод cancel().
Создание обработчика:
const updateMap = debounce(() => {
refreshLayers();
}, 1000);
Отмена:
updateMap.cancel();
Это полезно при:
Иногда необходимо выполнить накопленный вызов без ожидания окончания таймера.
updateMap.flush();
Пример:
const saveConfig = debounce(() => {
persistConfiguration();
}, 3000);
Перед закрытием окна:
saveConfig.flush();
Все изменения будут сохранены немедленно.
Большинство современных проектов используют Kepler.gl совместно с React.
Типичный пример:
import debounce from "lodash/debounce";
import { useMemo } from "react";
function SearchPanel() {
const search = useMemo(
() =>
debounce(value => {
console.log(value);
}, 500),
[]
);
return (
<input
onCha nge={e => search(e.target.value)}
/>
);
}
useMemo предотвращает создание нового экземпляра
debounce-функции при каждом рендере.
Необходимо корректно освобождать ресурсы.
Пример:
import { useEffect } from "react";
import debounce from "lodash/debounce";
function Component() {
const update = debounce(() => {
console.log("update");
}, 500);
useEffect(() => {
return () => {
update.cancel();
};
}, []);
return null;
}
Так предотвращаются утечки памяти и обращения к уже удалённым компонентам.
События перемещения карты генерируются очень часто.
Например:
map.on("move", event => {
updateViewport(event);
});
Во время перетаскивания карта может генерировать десятки событий в секунду.
Оптимизация:
const debouncedViewportUpdate = debounce(
viewport => {
saveViewport(viewport);
},
500
);
map.on("move", event => {
debouncedViewportUpdate(event.viewport);
});
В результате сохраняется только финальное положение карты.
Конфигурация может содержать:
Без дебаунсинга:
store.subscribe(() => {
saveState(store.getState());
});
Каждое изменение приводит к сохранению.
Гораздо эффективнее:
const saveStateDebounced = debounce(
state => {
saveState(state);
},
1000
);
store.subscribe(() => {
saveStateDebounced(store.getState());
});
Это значительно снижает количество операций записи.
Многие приложения синхронизируют состояние карты с адресной строкой.
Без оптимизации:
history.replaceState(
{},
"",
generateUrl(state)
);
Подобный код может вызываться сотни раз.
Решение:
const updateUrl = debounce(state => {
history.replaceState(
{},
"",
generateUrl(state)
);
}, 500);
Теперь адрес обновляется только после завершения изменений.
Некоторые системы отображают потоковые геоданные в режиме реального времени.
Пример:
socket.onmess age = event => {
processData(event.data);
};
Если поток слишком интенсивный, обновление визуализации может стать узким местом.
В таком случае применяется дебаунсинг обновления интерфейса:
const redraw = debounce(() => {
refreshMap();
}, 250);
Получение данных остаётся непрерывным, но перерисовка выполняется реже.
На практике используются следующие значения.
| Сценарий | Интервал |
|---|---|
| Поиск по тексту | 300–500 мс |
| Автодополнение | 200–400 мс |
| API-запросы | 500–1000 мс |
| Сохранение настроек | 1000–3000 мс |
| Синхронизация URL | 300–1000 мс |
| Обновление конфигурации карты | 500–2000 мс |
Конкретные значения зависят от объёма данных и требований к отзывчивости интерфейса.
Неверно:
input.addEventListener("input", e => {
debounce(() => {
search(e.target.value);
}, 500)();
});
Каждое событие создаёт новый таймер.
Правильно:
const debouncedSearch = debounce(
search,
500
);
input.addEventListener("input", e => {
debouncedSearch(e.target.value);
});
Неверно:
debounce(updateMap, 5000);
Пользователь будет воспринимать интерфейс как зависший.
Неверно:
debounce(updateMap, 10);
Подобный интервал практически не снижает нагрузку.
Особенно критично для React-компонентов.
Незавершённые таймеры могут приводить к:
При проектировании крупных геоинформационных систем целесообразно применять дебаунсинг на нескольких уровнях:
Особенно заметный эффект достигается при работе с многомиллионными наборами геоданных, где даже небольшое сокращение количества операций существенно улучшает производительность интерфейса и уменьшает нагрузку на инфраструктуру приложения.