Debounce и throttle

В JavaScript часто возникает необходимость контролировать частоту выполнения функций, особенно при обработке событий, таких как прокрутка страницы, изменение размеров окна или ввод текста пользователем. Два ключевых подхода для этого — debounce и throttle. Они позволяют оптимизировать производительность и предотвращают лишние вызовы функций, которые могут нагружать браузер.


Debounce

Debounce откладывает выполнение функции до тех пор, пока определённое время не пройдёт после последнего вызова. Это особенно полезно для действий, которые должны выполняться только после завершения серии событий.

Принцип работы

  1. Создаётся таймер.
  2. При каждом новом вызове функции предыдущий таймер сбрасывается.
  3. Функция выполняется только после того, как таймер истечёт и новых вызовов не поступало.

Пример реализации

function debounce(func, delay) {
    let timer;
    return function (...args) {
        clearTimeout(timer);
        timer = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

// Использование:
const handleResize = debounce(() => {
    console.log('Изменение размера окна завершено');
}, 300);

window.addEventListener('resize', handleResize);

В этом примере функция handleResize будет вызвана только один раз через 300 мс после того, как пользователь прекратил изменять размер окна.

Ключевые моменты

  • Подходит для событий, которые происходят часто, но обработка должна выполняться один раз после завершения активности.
  • Используется для оптимизации поисковых форм, автосохранения, ресайзов, скролла с окончанием движения.
  • Важно учитывать контекст вызова, поэтому часто используется func.apply(this, args).

Throttle

Throttle ограничивает частоту вызова функции, выполняя её не чаще определённого интервала времени, независимо от того, сколько раз произошло событие. Это полезно, когда нужно, чтобы функция выполнялась регулярно, но не слишком часто.

Принцип работы

  1. Сохраняется время последнего вызова функции.
  2. Функция вызывается только если прошёл установленный интервал времени.
  3. Все последующие вызовы в этот интервал игнорируются.

Пример реализации

function throttle(func, limit) {
    let lastCall = 0;
    return function (...args) {
        const now = Date.now();
        if (now - lastCall >= limit) {
            lastCall = now;
            func.apply(this, args);
        }
    };
}

// Использование:
const handleScroll = throttle(() => {
    console.log('Прокрутка страницы');
}, 200);

window.addEventListener('scroll', handleScroll);

В этом примере функция handleScroll будет вызываться не чаще одного раза каждые 200 мс, даже если пользователь активно скроллит страницу.

Ключевые моменты

  • Подходит для постоянно происходящих событий, где необходимо поддерживать регулярный отклик.
  • Идеально для анимаций при скролле, обновления индикаторов, API-запросов с ограничением частоты.
  • Отличие от debounce: функция выполняется во время активности, а не после её окончания.

Сравнение Debounce и Throttle

Параметр Debounce Throttle
Когда выполняется функция После завершения серии событий Регулярно с заданным интервалом
Цель Предотвратить лишние вызовы после серии событий Ограничить частоту вызовов во время активности
Примеры Автосохранение текста, поиск при вводе Скролл-эффекты, изменение позиции элементов

Продвинутые варианты

Debounce с немедленным вызовом (leading edge):

function debounce(func, delay, immediate = false) {
    let timer;
    return function (...args) {
        const callNow = immediate && !timer;
        clearTimeout(timer);
        timer = setTimeout(() => {
            timer = null;
            if (!immediate) func.apply(this, args);
        }, delay);
        if (callNow) func.apply(this, args);
    };
}

Такой вариант позволяет выполнить функцию сразу при первом вызове, а последующие игнорировать до окончания интервала.

Throttle с trailing вызовом:

function throttle(func, limit) {
    let lastCall = 0;
    let timer;
    return function (...args) {
        const now = Date.now();
        const remaining = limit - (now - lastCall);
        if (remaining <= 0) {
            clearTimeout(timer);
            lastCall = now;
            func.apply(this, args);
        } else if (!timer) {
            timer = setTimeout(() => {
                lastCall = Date.now();
                timer = null;
                func.apply(this, args);
            }, remaining);
        }
    };
}

Такой подход позволяет гарантировать выполнение последнего события, которое произошло до окончания интервала.


Использование с Popper.js

В библиотеке Popper.js debounce и throttle часто применяются для обновления позиции поппера при изменении окна или скролле:

import { createPopper } from '@popperjs/core';

const button = document.querySelector('#button');
const tooltip = document.querySelector('#tooltip');

const popperInstance = createPopper(button, tooltip, {
    placement: 'top',
});

const updatePopper = throttle(() => {
    popperInstance.update();
}, 100);

window.addEventListener('resize', updatePopper);
window.addEventListener('scroll', updatePopper);
  • throttle здесь предотвращает чрезмерные вызовы update() при быстром скролле.
  • Использование debounce было бы оправдано, если обновление требуется только после окончания прокрутки или ресайза.

Вывод

Debounce и throttle — это инструменты контроля частоты выполнения функций, которые помогают снижать нагрузку на браузер и поддерживать плавность работы интерфейсов. Их правильное применение особенно критично в сочетании с динамическими библиотеками вроде Popper.js, где точная и оптимальная позиция элементов зависит от частых событий на странице.