Debounce и throttle

Проблема частых событий в интерфейсе

Современные пользовательские интерфейсы активно используют события браузера: прокрутку страницы, изменение размера окна, ввод текста в поле, перемещение курсора и многие другие. Некоторые из этих событий генерируются очень часто — десятки и даже сотни раз в секунду.

Например:

  • событие input срабатывает при каждом вводе символа
  • событие scroll может срабатывать десятки раз за одну секунду прокрутки
  • событие resize вызывается при каждом изменении размеров окна
  • событие mousemove может вызываться сотни раз в секунду

Если при каждом таком событии выполнять сложную логику (запрос к серверу, вычисления, перерисовку компонентов), это может привести к:

  • ухудшению производительности
  • избыточным сетевым запросам
  • «подвисаниям» интерфейса
  • лишним вычислениям

Для решения этой проблемы используются техники debounce и throttle. В экосистеме Vue и библиотеке Element Plus эти подходы активно применяются при обработке пользовательских действий.


Debounce

Суть метода

Debounce — это техника, которая откладывает выполнение функции до тех пор, пока не пройдет определённое время с момента последнего вызова.

Другими словами:

функция выполняется только после того, как события перестали происходить.

Каждый новый вызов сбрасывает таймер.

Визуальная модель

События:

|----|----|----|----|----|

Debounce (задержка 500ms):

                    |---Выполнение---|

Функция выполнится один раз, когда поток событий прекратится.


Классическая реализация debounce

Простейшая реализация:

function debounce(fn, delay) {
  let timer = null

  return function (...args) {
    clearTimeout(timer)

    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

Разбор:

  1. создаётся переменная timer
  2. каждый вызов функции очищает предыдущий таймер
  3. устанавливается новый setTimeout
  4. функция выполнится только если новые события не произошли

Пример применения

Поиск в поле ввода.

Без debounce:

function handleSearch(query) {
  fetch(`/api/search?q=${query}`)
}

Если пользователь вводит слово “element”, произойдут запросы:

e
el
ele
elem
eleme
elemen
element

Итого 7 запросов к серверу.


Использование debounce

const debouncedSearch = debounce((query) => {
  fetch(`/api/search?q=${query}`)
}, 500)

Теперь запрос отправится только один раз после окончания ввода.


Debounce во Vue с Element Plus

Частая ситуация — поле поиска с компонентом ElInput.

<template>
  <el-input
    v-model="search"
    placeholder="Search"
    @input="onInput"
  />
</template>

Логика:

import { ref } from 'vue'

const search = ref('')

const onIn put = debounce((value) => {
  loadUsers(value)
}, 500)

function loadUsers(query) {
  console.log('request:', query)
}

Теперь запрос выполняется только после паузы во вводе.


Debounce при фильтрации таблицы Element Plus

Компонент ElTable часто используется вместе с фильтрацией.

Без debounce:

<el-input v-model="filter" @input="applyFilter" />
function applyFilter(value) {
  tableData.value = allData.filter(item =>
    item.name.includes(value)
  )
}

При больших таблицах это создаёт нагрузку.

Использование debounce:

const applyFilter = debounce((value) => {
  tableData.value = allData.filter(item =>
    item.name.includes(value)
  )
}, 300)

Фильтрация выполняется только после паузы.


Debounce в Composition API

При использовании watch:

watch(search, debounce((value) => {
  fetchUsers(value)
}, 500))

Такой подход часто применяется для:

  • автопоиска
  • фильтрации
  • автосохранения
  • автозаполнения

Throttle

Суть метода

Throttle ограничивает частоту вызова функции.

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


Визуальная модель

События:

|----|----|----|----|----|

Throttle (500ms):

|--Выполнение--|      |--Выполнение--|

Функция будет выполняться через фиксированные интервалы, даже если события продолжаются.


Реализация 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)
    }
  }
}

Логика:

  1. сохраняется время последнего вызова
  2. при каждом событии проверяется интервал
  3. функция вызывается только если прошло нужное время

Пример использования

Событие scroll

window.addEventListener('scroll', throttle(() => {
  console.log('scroll update')
}, 200))

Если пользователь быстро прокручивает страницу, обработчик выполнится максимум раз в 200ms.


Использование с Element Plus

Частая задача — реакция на прокрутку контейнера.

Например, бесконечная подгрузка данных в таблице.

const handleScroll = throttle(() => {
  if (isNearBottom()) {
    loadMore()
  }
}, 300)

В шаблоне:

<div @scroll="handleScroll">

Пример: виртуальный список

При создании больших списков с ElScrollbar события прокрутки происходят очень часто.

Throttle позволяет уменьшить количество вычислений:

const updateVisibleItems = throttle(() => {
  calculateVisibleRange()
}, 100)

Это снижает нагрузку на рендеринг.


Сравнение debounce и throttle

Характеристика Debounce Throttle
Принцип выполнение после паузы выполнение через интервалы
Реакция на события только последнее событие события ограничиваются по частоте
Количество вызовов минимальное регулярное
Основная задача ожидание окончания действий контроль частоты

Когда использовать debounce

Подходит для ситуаций, когда важно дождаться окончания действия:

  • ввод текста
  • поиск
  • автозаполнение
  • отправка формы
  • фильтрация таблиц
  • автосохранение

Примеры в интерфейсах Element Plus:

  • ElInput поиск
  • ElSelect удалённая загрузка
  • фильтры ElTable
  • формы ElForm

Когда использовать throttle

Подходит для постоянных потоков событий:

  • прокрутка
  • изменение размеров окна
  • перемещение курсора
  • отслеживание позиции

Типичные сценарии:

  • lazy loading
  • infinite scroll
  • анимации
  • обновление позиций элементов

Debounce и throttle через lodash

В реальных проектах часто используется библиотека lodash.

Установка:

npm install lodash

Импорт:

import debounce from 'lodash/debounce'
import throttle from 'lodash/throttle'

Пример debounce:

const handleSearch = debounce((value) => {
  loadUsers(value)
}, 400)

Пример throttle:

const handleScroll = throttle(() => {
  updateHeader()
}, 200)

Преимущества lodash:

  • проверенная реализация
  • дополнительные параметры
  • меньше вероятность ошибок

Дополнительные параметры debounce

Lodash поддерживает опции:

debounce(fn, wait, options)

Параметры:

  • leading
  • trailing
  • maxWait

leading

Функция вызывается сразу, а затем блокируется.

debounce(fn, 500, { leading: true })

trailing

Функция вызывается в конце задержки.

Это поведение используется по умолчанию.


maxWait

Гарантирует выполнение функции, даже если события продолжаются.

debounce(fn, 500, {
  maxWait: 2000
})

Типичная ошибка

Создание debounce внутри обработчика.

Неправильно:

function onInput(value) {
  debounce(() => {
    search(value)
  }, 300)
}

Каждый вызов создаёт новый debounce.

Правильно:

const debouncedSearch = debounce((value) => {
  search(value)
}, 300)

function onInput(value) {
  debouncedSearch(value)
}

Отмена debounce

Lodash предоставляет метод cancel.

const debounced = debounce(loadData, 500)

debounced.cancel()

Полезно при:

  • размонтировании компонента
  • смене страницы
  • отмене запросов

Использование при уничтожении компонента

Во Vue:

import { onUnmounted } from 'vue'

const debouncedSearch = debounce(loadUsers, 500)

onUnmounted(() => {
  debouncedSearch.cancel()
})

Это предотвращает выполнение функции после удаления компонента.


Debounce и реактивность Vue

Важно помнить: debounce работает с функциями, а не с реактивными переменными.

Поэтому чаще всего он используется вместе с:

  • watch
  • обработчиками событий
  • методами компонентов

Пример:

watch(search, debounce((value) => {
  fetchUsers(value)
}, 400))

Комбинация debounce и throttle

В сложных интерфейсах могут использоваться оба подхода.

Пример:

  • debounce — для поиска
  • throttle — для прокрутки списка результатов
const searchUsers = debounce(loadUsers, 400)

const handleScroll = throttle(loadMoreUsers, 200)

Такая комбинация позволяет одновременно:

  • уменьшить количество запросов
  • контролировать производительность интерфейса
  • избежать лишних вычислений

Практические сценарии в Element Plus

Наиболее частые области применения:

Поиск пользователей

ElInput + debounce

Фильтрация таблиц

ElTable + debounce

Бесконечная прокрутка

scroll + throttle

Изменение размеров

resize + throttle

Автосохранение форм

watch(form) + debounce

Производительность интерфейсов

Использование debounce и throttle значительно снижает нагрузку:

Без оптимизации:

  • сотни вызовов функций
  • десятки запросов
  • лишние перерисовки

С оптимизацией:

  • контролируемое число операций
  • плавная работа интерфейса
  • стабильная производительность

В больших приложениях на Vue и Element Plus эти техники являются стандартным инструментом оптимизации обработки пользовательских событий.