Throttling и debouncing — это широко используемые техники для увеличения производительности кода, который выполняется повторно с некоторой периодичностью.

Давайте разберёмся, как лучше их применять, чтобы ускорить работу ваших приложений.

Что это вообще такое?

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

Debouncing функции означает, что все вызовы будут игнорироваться до тех пор, пока они не прекратятся на определённый период времени. Только после этого функция будет вызвана. Например, если мы установим таймер на 2 секунды, а функция вызывается 10 раз с интервалом в одну секунду, то фактический вызов произойдёт только спустя 2 секунды после крайнего (десятого) обращения к функции.

Можно провести такую аналогию:

Предположим, вы работаете за компьютером и в то же время чатитесь с другом в мессенджере, который отправляет вам сообщение за сообщением. Вы получаете пуш уведомления каждую минуту. В обычном случае вы бы прочитывали эти сообщения сразу, но сейчас вы заняты и не можете так часто отвлекаться. Как можно поступить?

  1. Игнорировать уведомления, и прочитывать принятые сообщения раз в пять минут.
  2. Игнорировать уведомления. Если в течение последних пяти минут не поступало новых уведомлений, предположим, что друг закончил свою историю и тогда проверить принятые сообщения.

Первый вариант можно сравнить с throttling, а второй с debouncing.

Давайте разберёмся чем мотивировано использование троттлинга.

Для чего может понадобиться применять эти техники в коде?

Предположим, у вас есть событие «С», которое, при срабатывании, вызывает функцию «Ф». Обычно «Ф» вызывается при каждом срабатывании «С», и это нормально.

Но что, если «С» срабатывает слишком часто, например 200 раз в секунду? Если «Ф» выполняет какие-то простые вычисления, то и это нормально. Но если «Ф» выполняет «дорогие» операции, например вызов внешнего API, тяжёлые вычисления или сложные манипуляции с DOM, то вы захотите ограничить частоту вызова «Ф», чтобы не было проседания производительности. Другой случай, когда стоит ограничить частоту вызовов, — это если какой-либо другой компонент приложения зависит от результата «Ф».

Давайте рассмотрим два распространённых случая, когда используют троттлинг и debouncing.

  • Видео игры

В экшен играх приходиться нажимать кнопки с высокой частотой для выполнения какого-либо действия (стрельба, удар). Как правило игроки нажимают кнопки намного чаще чем это требуется, вероятно, увлекаясь происходящим. Таким образом игрок может нажать на кнопку «удара» 10 раз в течение пяти секунд, но персонаж делает не более одного удара в секунду. В этом случае троттлинг события «удар», позволяет игнорировать повторные нажатия кнопки в течение секунды.

  • Автозаполнение

В строке поиска часто реализуют автозаполнение для текущего ввода пользователя. Иногда предлагаемые варианты замены извлекаются из серверной части, через API (например, на Google Maps).

Google карты с реализацией debouncing

Предположим, вы вводите в строку поиска «Greenwich». API автозаполнения будет вызываться при изменении текста в строке поиска. Если бы не debouncing, вызов API выполнялся бы после каждой введённой буквы, даже если вы печатаете очень быстро.

У такого подхода есть две основные проблемы:

  1. Набирая слово «Green» пользователь будет получать предложения автозаполнения сначала для «G», потом для «Gr», «Gre» и т.д. Если пользователь печатает очень быстро, то это будет сбивать его с толку.
  2. Нет гарантии, что API запросы будут возвращены в том порядке, в котором они были отправлены. Например, запрос автозаполнения для «Gre» может вернуться после запроса для «Green». Это означает, что пользователь сначала увидит обновлённый список (предложения для «Green»), который затем будет заменён устаревшим (предложения для «Gre»).

Поэтому имеет смысл «притормозить» поиск. Debouncing функции автозаполнения на одну секунду позволит ограничить запросы, пока пользователь не перестанет печатать.

В конце концов троттлинг можно представить так: «Привет, похоже ты начал что-то делать, если ты хочешь продолжить, — нет проблем, я пока не буду обращать на это внимание». А debouncing, так: «Похоже ты ещё не закончил, ― продолжай, а я подожду».

Реализация Throttling и Debouncing

Давайте посмотрим, как можно реализовать простую функцию с троттлингом в JavaScript. Использовать её будем так:

// regular call to function handleEvent
element.on('event', handleEvent);

// throttle handleEvent so it gets called only once every 2 seconds (2000 ms)
element.on('event', throttle(handleEvent, 2000));

Отметим следующие моменты:

  1. Наша функция будет принимать два параметра: функцию для троттлинга вызовов и интервал задержки (в миллисекундах).
  2. Троттлинг-функция должна возвращать функцию. Эта функция вызывается при срабатывании события. Именно эта функция отслеживает вызовы функций и решает, следует ли вызывать исходную функцию.
  3. Чтобы отслеживать время последнего вызова функции, мы будем использовать особенность функций в JavaScript, которые являются по сути обычными объектами и могут иметь свойства. Мы можем использовать свойство lastCall, чтобы записывать время последнего вызова функции.
  4. Как определить, следует ли вызывать исходную функцию? Есть два сценария: 1. это был первый вызов, 2. время троттлинга истекло с момента последнего вызова функции.

Учитывая всё это, вот что мы получим (с примером использования):

function throttle(f, t) {
return function (args) {
let previousCall = this.lastCall;
this.lastCall = Date.now();
if (previousCall === undefined // function is being called for the first time
|| (this.lastCall - previousCall) > t) { // throttle time has elapsed
f(args);
}
}
}

let logger = (args) => console.log(`My args are ${args}`);
// throttle: call the logger at most once every two seconds
let throttledLogger = throttle(logger, 2000);

throttledLogger([1, 2, 3]);
throttledLogger([1, 2, 3]);
throttledLogger([1, 2, 3]);
throttledLogger([1, 2, 3]);
throttledLogger([1, 2, 3]);

// "My args are 1, 2, 3" - logged only once

Далее, простая debouncing функция, которая будет использоваться так:

// regular call to function handleEvent
element.on('event', handleEvent);

// debounce handleEvent so it gets called after calls have stopped for 2 seconds (2000 ms)
element.on('event', debounce(handleEvent, 2000));

Обратите внимание на следующие моменты:

  1. Debouncing подразумевает ожидание. Мы должны ждать, пока вызовы функции прекратятся на заданное время. Для реализации ожидания мы используем setTimeout.
  2. Так как мы не можем просто сказать функции: «подожди, пока вызовы не прекратятся», мы будем использовать setTimeout. Каждый раз, когда функция вызывается, мы планируем вызов исходной функции, если в течении определённого времени не было вызовов. Если вызовы были до истечения заданного нами времени, то мы отменяем ранее запланированный вызов и переносим его.
  3. Ещё раз, для отслеживания вызовов мы будем использовать свойства функции. Помимо lastCall, мы будем хранить свойство lastCallTimer. Так мы придержим значение, возвращаемое setTimeout, и сможем отменить таймер (с помощью clearTimeout), если это необходимо.

Финальный результат:

function debounce(f, t) {
return function (args) {
let previousCall = this.lastCall;
this.lastCall = Date.now();
if (previousCall && ((this.lastCall - previousCall) <= t)) {
clearTimeout(this.lastCallTimer);
}
this.lastCallTimer = setTimeout(() => f(args), t);
}
}

let logger = (args) => console.log(`My args are ${args}`);
// debounce: call the logger when two seconds have elapsed since the last call
let debouncedLogger = debounce(logger, 2000);

debouncedLogger([1, 2, 3]);
debouncedLogger([4, 5, 6]);
debouncedLogger([7, 8, 9]);

// "My args are 7, 8, 9" - logged after two seconds

Библиотека lodash может обеспечить более мощную throttle и debounce функциональность. С ней вы можете делать так:

element.on(‘scroll’, _.throttle(handleScroll, 100));

$(window).on(‘resize’, _.debounce(function() {
}, 100));

Если вам не нужна библиотека целиком, можно импортировать только субмодули:

const debounce = require('lodah/debounce');
const throttle = require('lodash/throttle');

// and use like before:
element.on(‘scroll’, throttle(handleScroll, 100));

$(window).on(‘resize’, debounce(function() {
}, 100));

Заключение

На сегодняшний день, использовать троттлинг и debouncing наиболее полезно во фронтенде, где мы не можем контролировать скорость действий пользователей. Такие функции могут быть полезны и для сервера. На API серверах часто реализуют троттлинг («ограничение скорости»), чтобы предотвратить перегрузку приложения.

Троттлинг наиболее эффективен, когда входные данные для вызова функции не имеют значения или одинаковы каждый раз (например, событие scroll), в то время как debouncing лучше всего подходит, когда результат последнего события (например, при изменении размера окна) имеет значение для конечного пользователя. Спасибо за внимание.

Перевод статьи ShalvahUnderstanding Throttling and Debouncing

Предыдущая статьяХитрости объектно-ориентированного программирования. Часть 3: Смерть от аргументов
Следующая статьяОвладей Python, создавая реальные приложения. Часть 4