Как и зачем использовать throttle и debounce из underscore или lodash

октябрь 14 , 2016
Метки:
Демо Исходники

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

Это одни из тех приемов, что я очень советую знать и использовать во фронтенд-разработке. Можете даже сразу посмотреть демо. Только не забудьте открыть консоль, чтобы понять, в чем дело. А всех желающих разобраться с этой темой подробнее прошу под кат.


Про throttle

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

Подобные операции красивы и нравятся нашим пользователям, но к сожалению, дают большую нагрузку на браузер и dom. Давайте для примера напишем простенький обработчик события ресайза окна.

    var _onResizeWindow = function() {
        console.log('width: ', $(window).width());
    }

    $(window).on('resize', _onResizeWindow);

Этот код всего лишь выводит в консоль ширину окна. Посмотреть можно здесь - демо-страничка. В блоке throttle выберите пункт "Простой ресайз окна". Попробуйте поиграть размером окна, и увидите, что эта функция срабатывает десятки раз даже на небольшое изменение ширины.

А теперь представьте, что в _onResizeWindow у нас написан не безобидный console.log, а ряд операций по сложным манипуляциям с dom - браузер взвоет, а иногда и начнет тормозить отрисовку. Возможно, Вам это покажется маловероятным, ведь у всех давно уже не третьи пентиумы. Но я не раз убеждался, что одной причин многих тормозов на сайтах является неоптимальная работа вот с такими незаметными мелочами, вроде скроллинга и ресайза. А ведь чтобы снизить нагрузку на браузер, нужно сделать самую малость - использовать _.throttle

    // Функция, выполняющаяся при ресайзе окна
    var _onResizeWindow = function() {
        console.log('width: ', $window.width());
    }
    // Та же функция, только с _.throttle
    var _onResizeWindowThrottled = _.throttle(_onResizeWindow, 300);

    $(window).on('resize', _onResizeWindowThrottled);

_.throttle создает обертку для искомой функции и дает ей указание запускаться не чаще, чем раз в N миллисекунд. В нашем случае укажем 300 - это стандартный промежуток времени, за который пользователи не замечают задержку интерфейса, а браузер успевает выполнить нужные манипуляции с dom. Конечно, без фанатизма, если на каждый чих перерисовывать все содержимое страницы, никакие ухищрения не помогут :-)

Вернемся к демо-странице. Выберите "Ресайз окна с _.throttle" и убедитесь, что функция срабатывает гораздо реже (для демонстрации установлено значение 500). Таким образом, как бы часто не вызывалось некоторое событие, _.throttle не даст нужной функции запускаться слишком часто и создавать лишнюю нагрузку на браузер.


Про debounce

А теперь рассмотрим еще один пример, связанный с производительностью фронтенда, и разберемся с _.debounce.

Представим, что у нас на сайте есть некое поле ввода, правильность данных в котором мы хотим проверять в режиме онлайн. Не когда пользователь кликнет на кнопку "Отправить", а прямо в процессе набора, чтобы как можно раньше сообщить ему об ошибке (конечно, нужно делать это ненавязчиво).

Рассмотрим конкретный пример: есть поле ввода email-а, на которое навешано событие keyup, запускающее проверку на валидность email-адреса. И если адрес не валиден, то выдаем сообщение об ошибке. Для примера мы выведем сообщение в консоль, причем с любым результатом проверки - опять же для наглядности.

    // Валидация email
    var isValidEmail = function(email) {
        return (/^([a-z0-9_\-]+\.)*[a-z0-9_\-]+@([a-z0-9][a-z0-9\-]*[a-z0-9]\.)+[a-z]{2,4}$/i).test(email);
    }

    // Валидация при вводе email
    var _onKeyupEmailInput = function() {
        var email = $('input').val(),
            isValid = isValidEmail(email),
            message = email + ' is ' + isValid;
        
        if (isValid) {
            console.info(message);
        } else {
            console.error(message);
        }
    }
    
    $('input').on('keyup', _onKeyupEmailInput);

На функцию isValidEmail не обращайте внимания, регулярка по-быстрому нагуглена в интернетах (впрочем, она работает достаточно четко). Посмотреть пример можно все на той же страничке демо. В разделе debounce выберите пункт "Простое нажатие кнопок". Теперь откройте консоль и начните набирать какой-нибудь email. В консоль сразу посыпятся ошибки console.error ровно до тех пор, пока не введете валидную почту.

И здесь видим 2 проблемы.

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

А вторая проблема еще хуже: допустим, мы не проверяем почту, а пишем сервис онлайн-переводов, вроде google translate. И делаем нужные проверки не на клиенте, а например, бомбим ajax-запросами на сервер. Что конечно же требует гораздо больше времени ожидания и впустую потраченных ресурсов сервера. Это когда вместо одной проверки на test@gmail.com сервер будет заниматься t, te, tes, test ... и так далее до искомого test@gmail.com.

В обоих случаях нам поможет такой прием: запускать нужную функцию (_onKeyupEmailInput) не сразу, а только спустя некоторое время после срабатывания события в браузере (у нас keyup). Это поможет посетителю спокойно набирать email, а когда он закончит и перестанет щелкать по клавишам, запустится функция валидации, которая уже и определит правильность введенного email-а. В любом случае при таком подходе гораздо больше вероятность, что код будет работать с теми данными, которые пользователь хочет нам сообщить, а не с наполовину введенными.

Надеюсь, проблема и способ решения понятны, теперь посмотрим, как это делается в коде - до ужаса похоже на пример с _.throttle

    ...
    var _onKeyupEmailInputDebounced = _.debounce(_onKeyupEmailInput, timeDebounce);
    $('input').on('keyup', _onKeyupEmailInputDebounced);

Добавили функцию-обертку и привязали ее в input keyup. Посмотрите, в чем отличия на демо-странице - выберите "Нажатие кнопок с _.debounce".

И это все, что я хотел рассказать о замечательных функциях throttle и debounce. Надеюсь, что убедил в разумности использования таких приемов на практике, и эти функции станут Вашими незаменимыми помощниками.


Ссылки из статьи

Демо приложения
Исходники - исходники страницы демо, более развернутые, чем написано в статье.

Что еще почитать о фронтенде

Демо Исходники
Метки:
Заходите в группу в контакте - https://vk.com/webdevkin
Анонсы статей, обсуждения интернет-магазинов, vue, фронтенда, php, гита.
Истории из жизни айти и обсуждение кода.
Как Вам статья? Оцените!