Повышаем производительность при resize, scroll, ...

Некоторые события в браузере происходят с очень высокой частотой. Количество событий при движении курсора, прокрутке страницы, изменении размеров окна браузера, зажатой клавиши и так далее может достигать 100 раз в секунду. Функции, вызываемые при такой частоте событий, создают большую нагрузку на браузер и производительность падает. Как оптимизировать данный процесс?

Ниже расположен элемент, при движении курсора на котором будет отображаться количество событий

Hover and move over the square

Total Events - исходное количество событий при перемещении курсора

Throttle Events - количество событий при использовании функции throttle. Считаются события, которые происходят через каждые 100 миллисекунд. Остальные события игнорируются

Debounce Start Events - количество событий при использовании функции debounce. Считаются события, когда курсор начал перемещение и в течении 100 миллисекунд еще не закончил перемещаться. Остальные события игнорируются

Debounce End Events - количество событий при использовании функции debounce. Считаются события, когда курсор закончил перемещаться и после завершения перемещения прошло 100 миллисекунд. Остальные события игнорируются

После перемещения курсора на элементе выше, видим что количество событий при использовании функций throttle и debounce значительно сокращается, что даёт меньшую нагрузку на браузер и следовательно прирост производительности

Ниже рассмотрим код функций throttle и debounce



Код функций Throttle и Debounce

Основная задача функций throttle и debounce ограничивать количество вызовов других функций при высокой частоте событий (resize, scroll, keydown)


Throttle

Игнорирует вызовы передаваемой функции, пока между событиями (resize, scroll, keydown) не пройдет заданное время ожидания

document.addEventListener('DOMContentLoaded', () => { // структура страницы загружена и готова к взаимодействию

  const throttle = (func, ms) => { // объявляем функцию throttle

    let locked = false // переменная которая отвечает за блокировку вызова функции

    return function() { // эта функция запускается при каждом событии движения курсора

      if (locked) return // если заблокировано, то прекращаем выполнение этой функции

      const context = this // запоминаем передаваемую функцию func
      const args = arguments // запоминаем параметры передаваемой функции func

      locked = true // блокируем вызов функции

      setTimeout(() => { // устанавливаем время ожидания

        func.apply(context, args) // выполняем переданную функцию func
        locked = false // снимаем блокировку

      }, ms) // подставляем значение параметра ms

    }
  }
})


Debounce

Игнорирует вызовы передаваемой функции, пока события (resize, scroll, keydown) продолжают повторяться в пределах заданного времени ожидания

document.addEventListener('DOMContentLoaded', () => { // структура страницы загружена и готова к взаимодействию

  function debounce(func, ms, now) { // объявляем функцию debounce

    let onLast // переменная отвечает за вызов функции func после того, как прошло время ожидания ms от последнего события движения курсора

    return function() { // эта функция запускается при каждом событии движения курсора

      const context = this // запоминаем передаваемую функцию func
      const args = arguments // запоминаем параметры передаваемой функции func

      const onFirst = now && !onLast // если хотим запустить функцию func при первом событии движения курсора и время ожидания не установлено

      clearTimeout(onLast) // сбрасываем время ожидания ms

      onLast = setTimeout(() => { // устанавливаем время ожидания

        onLast = null // очищаем переменную onLast
        if (!now) func.apply(context, args) // если при первом событии движения курсора функция func не была вызвана, то вызываем ее когда время ожидания ms закончилось

      }, ms) // подставляем значение параметра ms

      if (onFirst) func.apply(context, args) // запускаем функцию func при первом событии движения курсора

    }
  }
})


Логика Throttle и Debounce

Разберем небольшой абстрактный пример

Предположим, на странице есть индикатор прокрутки. При прокрутке страницы вниз - ширина индикатора прокрутки увеличивается, при прокрутке вверх - уменьшается. За расчет и отображение ширины отвечает функция updateProgressbar()

Мы можем вызывать функцию updateProgressbar() при каждом событии прокрутки страницы, то есть до 100 раз в секунду, но данный подход сильно нагрузит браузер частыми расчетами

document.addEventListener('scroll', updateProgressbar)


Можем ограничить количество вызовов функции updateProgressbar() при помощи функции throttle - функция updateProgressbar() будет вызываться один раз в секунду при прокрутке страницы. То есть при прокрутке страницы индикатор прокрутки будет обновлять ширину один раз в секунду

document.addEventListener('scroll', throttle(updateProgressbar, 1000))


Можем ограничить количество вызовов функции updateProgressbar() при помощи функции debounce - функция updateProgressbar() будет вызываться один раз, спустя одну секунду после каждого окончания прокрутки страницы. То есть пользователь начал прокручивать страницу - ширина индикатора пока не обновляется. Как только пользователь закончил прокручивать страницу и прошла одна секунда ожидания, обновляем ширину индикатора

document.addEventListener('scroll', debounce(updateProgressbar, 1000))


Можем ограничить количество вызовов функции updateProgressbar() при помощи функции debounce с дополнительным параметром немедленного вызова - функция updateProgressbar() будет вызываться один раз, как только пользователь начинает прокручивать страницу. То есть, как только пользователь начинает прокручивать страницу, ширина индикатора сразу же обновляется. Пользователь заканчивает прокрутку страницы, проходит секунда, и только когда пользователь снова начинает прокрутку страницы, ширина индикатора обновится

document.addEventListener('scroll', debounce(updateProgressbar, 1000, true))

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



Применение Throttle и Debounce

Основные примеры использования:

Отслеживание прокрутки страницы - пример с индикатором прокрутки, чтобы уменьшить количество расчетов, применяем функцию throttle

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

Сокращение количества ajax запросов - например, при вводе запроса в поле <input type="text"> используем функцию debounce с задержкой 200 миллисекунд - в таком случае запрос не будет отправляться на сервер после ввода каждой буквы, а будет отправлен один раз после того, как пользователь перестанет вводить запрос и пройдет 200 миллисекунд


Выбор функции зависит от задачи

  1. Если нужно, чтобы функция при каком-либо действии (resize, scroll, keydown) выполнялась с определенной периодичностью, то используем throttle

  2. Если необходим вызов функции по окончании какого-либо действия, то используем debounce

  3. Если необходим вызов функции в начале какого-либо действия, то используем debounce с дополнительным параметром немедленного вызова



Итоги

Данная статья скорее информативная, хотя и постарался, как можно подробнее описать работу функций throttle и debounce и их применение

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

По возможности статья будет дополняться



Полезные ссылки

Декораторы и переадресация вызова, call/apply