Используем Throttle и Debounce в React

andreysm

Andrey Smirnov

Posted on May 28, 2022

Используем Throttle и Debounce в React

Обзор

Throttle и Debounce решают задачи оптимизации.

Throttle - пропускает вызовы функции с определённой периодичностью.
Debounce - откладывает вызов функции до того момента, когда с последнего вызова пройдёт определённое количество времени.

Throttle & Debounce схема:

Image description

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

1) Если пользователь изменяет размер окна браузера и нам необходимо изменять содержимое сайта.
Без оптимизации происходит следующее. При каждом событии изменения размера окна вызывается обработчик события изменения размера окна. Таким образом, если пользователь, например, изменяет размер окна в течение 10 секунд, то может произойти 100, 200 и т.д. событий, которые нам нужно обработать.
Throttle позволяет нам задать временной интервал, чаще которого обработчик события вызываться не будет. Если мы, используя Throttle, укажем интервал в 1 секунду, то кол-во выполнения обработчиков события изменения размера окна будет равно 10.

2) Показ пользователю количества процентов прокрутки страницы. При прокрутке страницы пользователем возникают события scroll, которые нам необходимо обработать. С помощью throttle мы можем уменьшать кол-во обрабатываемых событий прокрутки пользователем страницы, задав временной интервал.

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

1) Обработка данных поискового запроса пользователя.
При введении пользователем поискового запроса, ему предлагаются варианты поиска. Это происходит следующим образом.
При изменении вводимого пользователем текста, на сервер передаётся запрос, в котором мы передаем уже напечатанные символы. Затем получаем ответ от сервера с возможными вариантами поискового запроса и показываем их пользователю.
При каждом изменении текста пользователем, вызывается обработчик события, в котором делается запрос на сервер.
Для оптимизации количества отправляемых запросов на сервер используем Debounce.
При изменении текста пользователем, применение Debounce позволяет нам создать таймер, например на 1 секунду. Если 1 секунда проходит, и пользователь не изменил текст второй раз, то вызывается обработчик события и выполняется запрос к серверу. Если пользователь изменяет текст второй раз за 1 секунду, то 1-й таймер сбрасывается и создаётся новый таймер опять на 1 секунду.
Таким образом, если пользователь редактирует поисковый текст быстро (быстрее 1 секунды), то запрос отправится на сервер только один раз, после того, как пользователь прекратит печатать.

2) Отправка данных аналитики на сервер. Например, пользователь водит мышкой по сайту, мы записываем координаты мышки в массив, после чего Debounce позволяет отправить информацию о перемещении мышки клиента на сервер только после того, как клиент перестаёт двигать мышкой.

Итак, в этой статье я покажу, как использовать Throttle и Debounce в React приложении.

Шаг 1 - Шаблон приложения

Создадим шаблон приложения с помощью create-react-app и запустим его:



npx create-react-app throttle-debounce
cd throttle-debounce
npm start


Enter fullscreen mode Exit fullscreen mode

Заменяем содержимое файла App.css своими стилями:



body {
    display: flex;
    justify-content: center;
    width: 100%;
}
h1 {
    text-align: center;
    margin: 0.5rem 0;
}
.l-scroll {
    overflow-y: scroll;
    overflow-x: hidden;
    width: 380px;
    height: 200px;
    margin-top: 0.5rem;
}
.scroll-content {
    width: 100%;
    background-color: bisque;
    padding: 0 1rem;
}
.l-scroll::-webkit-scrollbar {
    width: 10px;
    height: 8px;
    background-color: darkturquoise;
}
.l-scroll::-webkit-scrollbar-thumb {
    background-color: blueviolet;
}


Enter fullscreen mode Exit fullscreen mode

Заменим содержимое файла App.js на шаблон нашего приложения:



import './App.css';
import { useMemo } from 'react';

function App() {
    return (
        <>
            <h1>Throttle & Debounce</h1>
            <div className="l-scroll">
                <div className="scroll-content">
                    <TallContent />
                </div>
            </div>
        </>
    );
}

// Прокручиваемый контент большой высоты
function TallContent(){
    const dataElements = useMemo(() => {
        const genData = [];
        for(let i=1; i<=200; i++){
            genData.push(
                <div key={i}>Line: {i}</div>
            );
        }
        return genData;
    }, []);

    return(
        <>
            {dataElements}
        </>
    );
}

export default App;


Enter fullscreen mode Exit fullscreen mode

Шаблон приложения готов, приступим ко второму шагу - обычный обработчик событий прокрутки.

Шаг 2 - Обычный обработчик событий

Здесь мы добавим обычный обработчик для scroll событий и посчитаем количество вызовов этого обработчика при прокрутке пользователем элемента страницы.

Добавим состояние количества вывозов обработчика событий в App компонент:



// В начале файла
import { useState, useMemo } from 'react';
// Внутри компонента App
const [scrollHandleCount, setScrollHandleCount] = useState(0);


Enter fullscreen mode Exit fullscreen mode

Затем добавим обработчик события прокрутки, для этого добавим аттрибут onScroll на элемент под h1 заголовком:



// Было
<div className="l-scroll">
    ...
</div>

// Стало
<div className="l-scroll" onScroll={handleScroll}>
    ...
</div>


Enter fullscreen mode Exit fullscreen mode

Также добавим функцию обработки события handleScroll в компонент App:



function handleScroll(){
    handleUsualScroll();
}


Enter fullscreen mode Exit fullscreen mode

Внутри функции handleScroll мы поместили функцию в которой будет происходить обработка обычного события. Добавим эту функцию в наш App компонент:



function handleUsualScroll(){
    setScrollHandleCount((prevState) => {
        return ++prevState;
    });
}


Enter fullscreen mode Exit fullscreen mode

Осталось только показать состояние счётчика пользователю, для этого добавим строку кода под h1 заголовком:



<span>
   Usual scroll handle count: {scrollHandleCount}
</span>
<br />


Enter fullscreen mode Exit fullscreen mode

Теперь, при прокрутке элемента на странице, мы должны увидеть кол-во вызовов функции handleUsualScroll().

Image description

Полный код компонента App на данный момент:



function App() {
    const [scrollHandleCount, setScrollHandleCount] = useState(0);
    return (
        <>
            <h1>Throttle & Debounce</h1>
            <span>
                Usual scroll handle count: {scrollHandleCount}
            </span>
            <br />
            <div className="l-scroll" onScroll={handleScroll}>
                <div className="scroll-content">
                    <TallContent />
                </div>
            </div>
        </>
    );

    function handleScroll(){
        handleUsualScroll();
    }
    function handleUsualScroll(){
        setScrollHandleCount((prevState) => {
            return ++prevState;
        });
    }    
}


Enter fullscreen mode Exit fullscreen mode

Шаг 3 - Обработчик событий с Throttle

Throttle обработчик событий в нашем случае должен вызывать увеличение счётчика scrollThrottleHandleCount, при этом пропускать вызовы увеличения счётчика с определённой периодичностью.
Для реализации задуманного нам потребуется таймер при запуске которого состояние Throlle переходит в In progress. При этом если состояние In Progerss, то обработка событий пользователя (прокрутка элемента страницы) пропускается.
Как только таймер срабатывает, состояние Throttle переходив в Not in progress, а значит наш обработчик будет снова обрабатывать события пользователя. Таким образом происходит пропуск событий пользователя с заданным интервалом времени.
Реализуем вышеописанное:



// Добавим useRef для хранения состояния inProgress
import { useState, useRef, useMemo } from 'react';


Enter fullscreen mode Exit fullscreen mode

Далее в компоненте App добавим состояние счётчика вызовов обработчика событий с Throttle и ref для хранения состояния inProgress:



// Кол-во вызовов обработчика событий с Throttle
const [
   scrollThrottleHandleCount,
   setScrollThrottleHandleCount
] = useState(0);
// Храним состояние in progress
const throttleInProgress = useRef();


Enter fullscreen mode Exit fullscreen mode

Здесь важно отменить, что throttleInProgress - часть сайд эффекта связанного с таймером, а значит, состояние мы будем хранить в ref объекте, так как useRef возвращает объект существующий на протяжении всего жизненного цикла компонента, при этом не происходит лишнего рендеринга компонента при изменении свойства current объекта возвращаемого useRef, в отличии от useState.
Теперь добавим сам обработчик события с Throttle в App component:



function handleThrottleScroll(){
    // Если состояние inProgress - выходим из функции,
    // пропускаем обработку события 
    if(throttleInProgress.current){ return; }
    // Устанавливаем inProgress в true и запускаем таймер
    throttleInProgress.current = true;
    setTimeout(() => {
        // Увеличиваем состояние throttleHandleCount
        // на единицу
        setScrollThrottleHandleCount((prevState) => {
            return ++prevState;
        });
        // inProgress устанавливаем в false,
        // значит при следующем запуске
        // setTimeout снова сработает
        throttleInProgress.current = false;
    }, 500);
}


Enter fullscreen mode Exit fullscreen mode

Осталось 2 простых действия: добавить отображение состояния счётчика с Throttle пользователю и добавить handleThrottleScroll() в handleScroll():



// После заголовка h1
<span>
   Throttle scroll handle count: {scrollThrottleHandleCount}
</span>

// В функцию handleScroll() после handleUsualScroll();
handleThrottleScroll();


Enter fullscreen mode Exit fullscreen mode

В результате мы получим:

Image description
Обычный обработчик событий вызвал бизнес логику приложения 181 раз, а с Throttle всего 9.
Полный код компонента App с Throttle:



function App() {
    const [scrollHandleCount, setScrollHandleCount] = useState(0);
    const [
        scrollThrottleHandleCount,
        setScrollThrottleHandleCount
    ] = useState(0);
    const throttleInProgress = useRef();

    return (
        <>
            <h1>Throttle & Debounce</h1>
            <span>
                Usual scroll handle count: {scrollHandleCount}
            </span>
            <br />
            <span>
                Throttle scroll handle count: {scrollThrottleHandleCount}
            </span>
            <br />
            <div className="l-scroll" onScroll={handleScroll}>
                <div className="scroll-content">
                    <TallContent />
                </div>
            </div>
        </>
    );

    function handleScroll(){
        handleUsualScroll();
        handleThrottleScroll();
    }
    function handleUsualScroll(){
        setScrollHandleCount((prevState) => {
            return ++prevState;
        });
    }
    function handleThrottleScroll(){
        if(throttleInProgress.current){ return; }
        throttleInProgress.current = true;
        setTimeout(() => {
            setScrollThrottleHandleCount((prevState) => {
                return ++prevState;
            });
            throttleInProgress.current = false;
        }, 500);
    }
}


Enter fullscreen mode Exit fullscreen mode

Перейдём к заключительному шагу - реализуем Debounce обработчик событий.

Шаг 4 - Обработчик событий с Debounce

Debounce в нашем примере откладывает увеличение счётчика scrollDebounceHandleCount до того момента, когда с последнего вызова обработчика события пройдёт определённое количество времени.
Добавим состояние количества вызовов обработчика событий с Debounce, ref для хранения идентификатора таймера в App компонент:



const [
    scrollDebounceHandleCount,
    setScrollDebounceHandleCount
] = useState(0);
const timerDebounceRef = useRef();


Enter fullscreen mode Exit fullscreen mode

Затем покажем количество scrollDebounceHandleCount пользователю и добавим наш метод handleDebounceScroll() в handleScroll():



// После h1
<span>
    Debound scroll handle count: {scrollDebounceHandleCount}
</span>
// В функцию handleScroll()
handleDebounceScroll();


Enter fullscreen mode Exit fullscreen mode

Осталось написать саму функцию handleDebounceScroll:



function handleDebounceScroll(){
    // Если ID таймена установлено - сбрасываем таймер
    if(timerDebounceRef.current){
        clearTimeout(timerDebounceRef.current);
    }
    // Запускаем таймер, возвращаемое ID таймера
    // записываем в timerDebounceRef
    timerDebounceRef.current = setTimeout(() => {
        // Вызываем увеличение счётчика кол-ва
        // выполнения бизнес логики приложения с Debounce
        setScrollDebounceHandleCount((prevState) => {
            return ++prevState;
        });
    }, 500);
}


Enter fullscreen mode Exit fullscreen mode

В результате увеличение счётчика с Debounce происходит только тогда, когда пользователь перестаёт прокручивать элемент страницы больше или равным 500 миллисекунд:

Image description
Полный текст App компонента:



function App() {
    const [scrollHandleCount, setScrollHandleCount] = useState(0);
    const [
        scrollThrottleHandleCount,
        setScrollThrottleHandleCount
    ] = useState(0);
    const [
        scrollDebounceHandleCount,
        setScrollDebounceHandleCount
    ] = useState(0);

    const throttleInProgress = useRef();
    const timerDebounceRef = useRef();

    return (
        <>
            <h1>Throttle & Debounce</h1>
            <span>
                Usual scroll handle count: {scrollHandleCount}
            </span>
            <br />
            <span>
                Throttle scroll handle count: {scrollThrottleHandleCount}
            </span>
            <br />
            <span>
                Debound scroll handle count: {scrollDebounceHandleCount}
            </span>
            <div className="l-scroll" onScroll={handleScroll}>
                <div className="scroll-content">
                    <TallContent />
                </div>
            </div>
        </>
    );

    function handleScroll(){
        handleUsualScroll();
        handleThrottleScroll();
        handleDebounceScroll();
    }
    function handleUsualScroll(){
        setScrollHandleCount((prevState) => {
            return ++prevState;
        });
    }
    function handleThrottleScroll(){
        if(throttleInProgress.current){ return; }
        throttleInProgress.current = true;
        setTimeout(() => {
            setScrollThrottleHandleCount((prevState) => {
                return ++prevState;
            });
            throttleInProgress.current = false;
        }, 500);
    }
    function handleDebounceScroll(){
        if(timerDebounceRef.current){
            clearTimeout(timerDebounceRef.current);
        }
        timerDebounceRef.current = setTimeout(() => {
            setScrollDebounceHandleCount((prevState) => {
                return ++prevState;
            });
        }, 500);
    }
}


Enter fullscreen mode Exit fullscreen mode

Подписывайтесь на блог, ставьте лайки, добавляйте в закладки.
Не забываем про единорогов.

Спасибо за внимание.

💖 💪 🙅 🚩
andreysm
Andrey Smirnov

Posted on May 28, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related