В пользовательских интерфейсах на JavaScript часто возникают ситуации, когда одно и то же событие вызывается десятки или сотни раз подряд. Типичные примеры — прокрутка страницы, изменение размеров окна, ввод текста в поле формы или перемещение курсора мыши. Если на каждое такое событие запускать тяжёлую функцию обработки, производительность интерфейса быстро ухудшается.
Библиотека Tabbable, применяемая для определения элементов, доступных для фокусировки через клавиатуру, обычно используется в сложных интерфейсах: модальные окна, выпадающие меню, диалоговые панели, сложные формы. В подобных компонентах часто требуется динамически пересчитывать список фокусируемых элементов. При частых событиях (например, изменении DOM или ресайзе) без оптимизации это приводит к избыточным вычислениям.
Для решения подобных проблем используются два фундаментальных механизма управления частотой вызовов функций:
Оба подхода ограничивают количество выполнений функции, но делают это по разным принципам.
Debounce откладывает выполнение функции до тех пор, пока события не перестанут происходить в течение заданного времени.
Алгоритм работает следующим образом:
Иными словами, функция выполняется один раз после завершения серии событий.
Пусть установлен интервал 300 мс.
События: | | | | | | | |
Время: 0 50 100 150 200 800 850
Debounce: ● ●
Функция запускается только после паузы.
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
Ключевые элементы реализации:
Поле поиска с автодополнением:
const handleInput = debounce(function(event) {
search(event.target.value);
}, 300);
input.addEventListener("input", handleInput);
Без debounce запросы отправлялись бы на каждый ввод символа. С debounce запрос выполняется только после паузы в печати.
В сложных интерфейсах список tabbable-элементов может пересчитываться при:
Если пересчёт выполняется слишком часто, производительность падает.
Пример пересчёта tabbable-элементов после изменения DOM:
import { tabbable } from "tabbable";
const updateTabbableElements = debounce(() => {
const nodes = tabbable(container);
console.log(nodes);
}, 200);
Теперь пересчёт выполняется только после завершения серии изменений.
Иногда требуется дополнительная функциональность:
Расширенная реализация:
function debounce(fn, delay, immediate = false) {
let timer = null;
function debounced(...args) {
const callNow = immediate && !timer;
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (!immediate) {
fn.apply(this, args);
}
}, delay);
if (callNow) {
fn.apply(this, args);
}
}
debounced.cancel = () => {
clearTimeout(timer);
timer = null;
};
return debounced;
}
Throttle ограничивает частоту выполнения функции, позволяя ей запускаться не чаще одного раза за заданный интервал времени.
Если события происходят чаще, лишние вызовы игнорируются или откладываются.
Интервал — 300 мс.
События: | | | | | | | | |
Время: 0 50 100 150 200 250 300 350
Throttle: ● ● ●
Функция выполняется регулярно, но не чаще установленного интервала.
function throttle(fn, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
fn.apply(this, args);
}
};
}
Здесь используется проверка времени последнего вызова.
Отслеживание прокрутки страницы:
const handleScroll = throttle(() => {
console.log(window.scrollY);
}, 200);
window.addEventListener("scroll", handleScroll);
Даже если событие scroll происходит сотни раз в секунду,
обработчик будет выполняться не чаще одного раза в 200 мс.
При сложных интерфейсах часто требуется отслеживать:
Например, список фокусируемых элементов может зависеть от видимости элементов внутри прокручиваемого контейнера.
import { tabbable } from "tabbable";
const updateFocusable = throttle(() => {
const elements = tabbable(container);
cache = elements;
}, 200);
container.addEventListener("scroll", updateFocusable);
Throttle позволяет регулярно обновлять данные, не перегружая приложение.
| Характеристика | Debounce | Throttle |
|---|---|---|
| Когда выполняется | после паузы между событиями | через фиксированные интервалы |
| Количество запусков | обычно один | регулярные |
| Подходит для | ввода текста, автопоиска | прокрутки, resize |
| Реакция на поток событий | ждёт окончания | ограничивает частоту |
Используется когда важно реагировать только на конечный результат действий.
Примеры:
В контексте tabbable debounce полезен для:
Используется когда требуется регулярная реакция на поток событий.
Примеры:
В интерфейсах с tabbable throttle применяют при:
Иногда применяется комбинация двух подходов.
Например:
Пример:
const update = () => {
focusableCache = tabbable(container);
};
const throttledUpdate = throttle(update, 200);
const debouncedUpdate = debounce(update, 300);
window.addEventListener("scroll", throttledUpdate);
window.addEventListener("scrollend", debouncedUpdate);
Такая схема уменьшает нагрузку и обеспечивает точный финальный результат.
thisПри использовании debounce и throttle важно сохранять контекст вызова.
Использование apply или call решает
проблему:
fn.apply(this, args);
Если не передавать аргументы, функция может работать некорректно.
Правильный способ:
function (...args) {
fn.apply(this, args);
}
Если debounce-функция содержит таймер, а компонент удаляется из DOM, таймер может остаться активным.
Решение — поддержка метода отмены:
debounced.cancel();
Во многих проектах используются готовые версии из библиотек:
lodash.debouncelodash.throttleОни поддерживают:
Пример:
import debounce from "lodash.debounce";
const update = debounce(() => {
const nodes = tabbable(container);
}, 200);
В интерфейсах, использующих Tabbable, пересчёт фокусируемых элементов может быть дорогой операцией, особенно если:
display, visibility,
tabindexПоэтому оптимизация вызовов функций с помощью debounce и throttle становится важной частью архитектуры интерфейса.
Правильное ограничение частоты выполнения: