DEV Community

Cover image for Используем Throttle и Debounce в React
Andrey Smirnov
Andrey Smirnov

Posted on

11 3

Используем 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

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

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

SurveyJS custom survey software

Build Your Own Forms without Manual Coding

SurveyJS UI libraries let you build a JSON-based form management system that integrates with any backend, giving you full control over your data with no user limits. Includes support for custom question types, skip logic, an integrated CSS editor, PDF export, real-time analytics, and more.

Learn more

Top comments (1)

Collapse
 
anastasiakot profile image
Anastasia Kotelnikova

спасибо за статью, полезно!

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more