Как использовать Throttle и Debounce в React для повышения производительности


Обзор

Throttle и Debounce решают проблемы оптимизации.

Throttle – пропускает вызовы функций с определенной частотой.

Схема Throttle & Debounce:

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

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

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

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

1) Обработка данных поискового запроса пользователя.
Когда пользователь вводит поисковый запрос, ему предлагаются варианты поиска. Это происходит следующим образом.
При изменении текста, введенного пользователем, на сервер отправляется запрос, в который мы передаем уже напечатанные символы. Затем мы получаем от сервера ответ с возможными вариантами поискового запроса и показываем их пользователю.
Каждый раз, когда пользователь меняет текст, вызывается обработчик события, в котором отправляется запрос на сервер.
Чтобы оптимизировать количество запросов, отправляемых на сервер, мы используем Debounce.
Когда текст изменяется пользователем, использование Debounce позволяет нам создать таймер, например, на 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
Войдите в полноэкранный режим Выйти из полноэкранного режима

Заменяем содержимое файла 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;
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Заменим содержимое файла 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>
        </>
    );
}

// High height scrollable content
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;
Войти в полноэкранный режим Выход из полноэкранного режима

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

Шаг 2 – хендлер обычных событий

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

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

// At the beginning of the file
import { useState, useMemo } from 'react';
// Inside the App component
const [scrollHandleCount, setScrollHandleCount] = useState(0);
Вход в полноэкранный режим Выход из полноэкранного режима

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

// Before
<div className="l-scroll">
    ...
</div>

// After
<div className="l-scroll" onScroll={handleScroll}>
    ...
</div>
Вход в полноэкранный режим Выйти из полноэкранного режима

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

function handleScroll(){
    handleUsualScroll();
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

function handleUsualScroll(){
    setScrollHandleCount((prevState) => {
        return ++prevState;
    });
}
Вход в полноэкранный режим Выйти из полноэкранного режима

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

<span>
   Usual scroll handle count: {scrollHandleCount}
</span>
<br />
Вход в полноэкранный режим Выйти из полноэкранного режима

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

Полный код компонента 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;
        });
    }    
}
Вход в полноэкранный режим Выход из полноэкранного режима

Шаг 3 – Обработчик событий с помощью Throttle

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

Мы реализуем вышеописанное:

// Add useRef to store inProgress state
import { useState, useRef, useMemo } from 'react';
Вход в полноэкранный режим Выход из полноэкранного режима

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

// Number of event handler calls with Throttle
const [
   scrollThrottleHandleCount,
   setScrollThrottleHandleCount
] = useState(0);
// Keeping the state in progress
const throttleInProgress = useRef();
Вход в полноэкранный режим Выход из полноэкранного режима

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

function handleThrottleScroll(){
    // If the state is inProgress - exit the function,
    // skip event processing
    if(throttleInProgress.current){ return; }
    // Set inProgress to true and start the timer
    throttleInProgress.current = true;
    setTimeout(() => {
        // Increment the throttleHandleCount
        // state by one
        setScrollThrottleHandleCount((prevState) => {
            return ++prevState;
        });
        // Set inProgress to false, which means
        // that setTimeout will work
        // again on the next run
        throttleInProgress.current = false;
    }, 500);
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

// After heading h1
<span>
   Throttle scroll handle count: {scrollThrottleHandleCount}
</span>

// In the handleScroll() function after handleUsualScroll();
handleThrottleScroll();
Вход в полноэкранный режим Выйти из полноэкранного режима

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

Обычный обработчик событий вызвал бизнес-логику приложения 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);
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Перейдем к последнему шагу – реализации обработчика события Debounce.

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

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

const [
    scrollDebounceHandleCount,
    setScrollDebounceHandleCount
] = useState(0);
const timerDebounceRef = useRef();
Вход в полноэкранный режим Выход из полноэкранного режима

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

// After h1
<span>
    Debound scroll handle count: {scrollDebounceHandleCount}
</span>
// In handleScroll() function
handleDebounceScroll();
Вход в полноэкранный режим Выход из полноэкранного режима

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

function handleDebounceScroll(){
    // If the timer ID is set, reset the timer
    if(timerDebounceRef.current){
        clearTimeout(timerDebounceRef.current);
    }
    // We start the timer, the returned timer ID
    // is written to timerDebounceRef
    timerDebounceRef.current = setTimeout(() => {
        // Increasing the counter for the number of
        // executions of the business logic
        // of the application with Debounce
        setScrollDebounceHandleCount((prevState) => {
            return ++prevState;
        });
    }, 500);
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

Полный текст компонента 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);
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

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

Оцените статью
Procodings.ru
Добавить комментарий