Используем Throttle и Debounce в React
Andrey Smirnov
Posted on May 28, 2022
Обзор
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
Заменяем содержимое файла 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>
</>
);
}
// Прокручиваемый контент большой высоты
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
компонент:
// В начале файла
import { useState, useMemo } from 'react';
// Внутри компонента App
const [scrollHandleCount, setScrollHandleCount] = useState(0);
Затем добавим обработчик события прокрутки, для этого добавим аттрибут onScroll
на элемент под h1
заголовком:
// Было
<div className="l-scroll">
...
</div>
// Стало
<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
, а значит наш обработчик будет снова обрабатывать события пользователя. Таким образом происходит пропуск событий пользователя с заданным интервалом времени.
Реализуем вышеописанное:
// Добавим useRef для хранения состояния inProgress
import { useState, useRef, useMemo } from 'react';
Далее в компоненте App
добавим состояние счётчика вызовов обработчика событий с Throttle
и ref
для хранения состояния inProgress
:
// Кол-во вызовов обработчика событий с Throttle
const [
scrollThrottleHandleCount,
setScrollThrottleHandleCount
] = useState(0);
// Храним состояние in progress
const throttleInProgress = useRef();
Здесь важно отменить, что 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);
}
Осталось 2 простых действия: добавить отображение состояния счётчика с Throttle
пользователю и добавить handleThrottleScroll()
в handleScroll()
:
// После заголовка h1
<span>
Throttle scroll handle count: {scrollThrottleHandleCount}
</span>
// В функцию handleScroll() после 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()
:
// После h1
<span>
Debound scroll handle count: {scrollDebounceHandleCount}
</span>
// В функцию handleScroll()
handleDebounceScroll();
Осталось написать саму функцию handleDebounceScroll
:
function handleDebounceScroll(){
// Если ID таймена установлено - сбрасываем таймер
if(timerDebounceRef.current){
clearTimeout(timerDebounceRef.current);
}
// Запускаем таймер, возвращаемое ID таймера
// записываем в timerDebounceRef
timerDebounceRef.current = setTimeout(() => {
// Вызываем увеличение счётчика кол-ва
// выполнения бизнес логики приложения с Debounce
setScrollDebounceHandleCount((prevState) => {
return ++prevState;
});
}, 500);
}
В результате увеличение счётчика с Debounce
происходит только тогда, когда пользователь перестаёт прокручивать элемент страницы больше или равным 500 миллисекунд:
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);
}
}
Подписывайтесь на блог, ставьте лайки, добавляйте в закладки.
Не забываем про единорогов.
Спасибо за внимание.
Posted on May 28, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.