DEV Community

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

Posted on • Edited on

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

Обзор

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

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

Throttle & Debounce схема:

Примеры использования 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';

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

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

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

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:

const handleScroll = () => {
  handleUsualScroll();
};
Enter fullscreen mode Exit fullscreen mode

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

const handleUsualScroll = () => {
  setScrollHandleCount(prevState => ++prevState);
};
Enter fullscreen mode Exit fullscreen mode

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

<span>
  Usual scroll handle count: {scrollHandleCount}
</span>
<br />
Enter fullscreen mode Exit fullscreen mode

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

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

const App = () => {
  const [scrollHandleCount, setScrollHandleCount] = useState(0);

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

  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>
    </>
  );
};

export default App;
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, useEffect } from 'react';
Enter fullscreen mode Exit fullscreen mode

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

// Кол-во вызовов обработчика событий с Throttle
const [
  scrollThrottleHandleCount,
  setScrollThrottleHandleCount
] = useState(0);
// Храним состояние in progress
const throttleInProgress = useRef();
// Храним id таймера для его удаления
// после размонтирования компонента
const throttleTimerId = useRef();
Enter fullscreen mode Exit fullscreen mode

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

const handleThrottleScroll = () => {
  // Если состояние inProgress - выходим из функции,
  // пропускаем обработку события 
  if(throttleInProgress.current) return;
  // Устанавливаем inProgress в true и запускаем таймер
  throttleInProgress.current = true;
  throttleTimerId.current = setTimeout(() => {
    // Увеличиваем состояние throttleHandleCount
    // на единицу
    setScrollThrottleHandleCount(prevState => ++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

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


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

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

  useEffect(() => {
    // Отчистка таймера при размонтировании компонента
    return () => {
      if (throttleTimerId.current) {
        clearTimeout(throttleTimerId.current);
      }
    };
  }, []);

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

  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>
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

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

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

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

const [
  scrollDebounceHandleCount,
  setScrollDebounceHandleCount
] = useState(0);
const debounceTimerIdRef = 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:

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

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


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

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

  const throttleInProgress = useRef();
  const throttleTimerId = useRef();
  const debounceTimerIdRef = useRef();

  useEffect(() => {
    // Отчистка таймера при размонтировании компонента
    return () => {
      if (throttleTimerId.current) {
        clearTimeout(throttleTimerId.current);
      }
      if (debounceTimerIdRef.current) {
        clearTimeout(debounceTimerIdRef.current)
      }
    };
  }, []);

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

  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>
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

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

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

Top comments (1)

Collapse
 
anastasiakot profile image
Anastasia Kotelnikova

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