⏰ Les fonctions Debounce et Throttle : comment ça fonctionne ?
Tags: javascript, programming, tutorial, react
Published: 2023-10-17 07:05:28

Intégrées dans les applications web / mobiles & logiciels bureau, elles se cachent souvent derrière un champ de recherche pour habriter client/serveur d'une pluie de requêtes indésirables, voir même dans le défilement vertical d'une page pour épargner une infinité d'opérations inutiles à votre navigateur.

Les fonctions **Debounce** et **Throttle** régulent au quotidien les performances de nos applications, mais comment fonctionnent t-elles ? Quelle approche aborder en utilisant React ?  

---

## 🔄 La fonction « Debounce »

La fonction « Debounce » **permet de limiter le nombre d'exécution d'une fonction** en l'exécutant **une seule fois** à terme d'un délai donné, sous réserve que la fonction ne soit pas réinvoquée, le cas échéant, **le délai est alors renouvellé**.

Elle consiste à **réguler le nombre d'appels à une fonction** en l'exécutant une seule fois, **à terme d'une série d'évènements**.

### A quel moment est-ce nécessaire ?

Pour comprendre l'intérêt du debounce, nous utiliserons l'exemple d'**une barre de recherche** grâce à laquelle nous pourrions **suggérer des résultats au fur et à mesure de notre saisie** _(sans avoir à utiliser de bouton de validation)_.

```
import React, {useState} from "react";

const Debounce = () => {
    const [searchValue, setSearchValue] = useState('');
    
    // Fetch articles with HTTP Request
    const fetchArticles = () => {
        console.log('fetchArticles() is called!');
    }
    
    /* Handle the value of search input */
    const handleSearchValue = (e) => {
        const newValue = e.target.value;
        setSearchValue(newValue);
        fetchArticles(); // Request to fetch articles
    }
    
    return (
        <div>
            <input 
                type="text" 
                name="searchValue" 
                value={searchValue}
                onChange={handleSearchValue} 
                placeholder="Votre recherche ici.."
            />
            <br />
            {searchValue && (
                <p>Voici les articles correspondant à:<br/><b>« {searchValue} »</b></p>
                /* ... Display articles ... */
            )}
        </div>
    );
}

export default Debounce;
```

La première idée à nous venir à l'esprit, et généralement la plus simple à mettre en oeuvre : se mettre à l'écoute d'un input de type texte et suggérer des articles chaque fois que le texte change.

> ℹ️ Dans l'exemple ci-dessus, l'état `searchValue` a pour vocation d'être synchronisé avec le champ de recherche. Il sera actualisé selon la valeur de notre input grâce à la callback `handleSearchValue`.

Si le résultat escompté semble fonctionnel, il convient cependant d'admettre que le fetch permettant de récupérer nos articles via l'utilisation de la fonction `fetchArticles(..)` sera quant à lui effectué autant de fois que la valeur de notre champ de recherche sera modifiée.

La conséquence de cette maladresse : de nombreux problèmes de performance menaçant directement l'intégrité de votre application/serveur.

En effet, supposons qu'un internaute effectue la recherche suivante: 

> « Debounce et throttle, comment ça fonctionne ? »

La callback `handleSearchValue` et la fonction `fetchArticles(..)` sont alors invoquées **45 fois**.

Si l'on suppose que la requête effectuée pour récupérer les articles auprès de notre serveur requiert **70.0kB**, pas moins de **3,15Mo** seront en réalité téléchargés à travers l'exécution de **45 requêtes**.

<u>Résultat de la recherche</u> : **3,08Mo** téléchargé inutilement.

C'est donc à l'occasion de l'exécution de notre fonction `fetchArticles(..)` que le debounce va être nécessaire et ainsi permettre une seule et unique exécution de notre requête, à terme de la saisie de l'utilisateur et non pas à chaque fois que la valeur de notre champ de recherche est modifiée.


### Comment ça fonctionne ?

**🟨 Approche globale en Javascript**

La fonction Debounce nécessite généralement deux paramètres :

- Une callback, nous utiliserons `cb` _(qui sera retournée sous condition)_.
- Un temps donné, nous utiliserons `delay` _(en millisecondes)_.

```
const debounce = (cb, delay = 1000) => {
    let timer;
    return (...args) => {
        clearTimeout(timer); // Clear of timeout if exist
        timer = setTimeout(() => {
            cb(...args); // Return the callback
        }, delay);
    };
};
```

La fonction `debounce(..)` à pour rôle d'initialiser un timer selon `delay` et de le réinitialiser chaque fois que la fonction est appelée. Lorsque le timer arrive à échéance, il retourne la callback `cb` fournie en premier paramètre qui permet son exécution.

Ainsi, la callback `cb` ne sera exécutée qu'à terme du delai spécifié si aucun ré-appel de la fonction ne survient d'ici là.

> _Note : La fonction debounce est fournie dans de nombreuses librairies Javascript dont [lodash](https://lodash.com/docs/4.17.15#debounce), la plus connue_.

**⚙️ Hook React personnalisé <u>useDebounce</u>**

La fonction debounce peut parfois hériter de paramètres complémentaires, ça peut être le cas notament lors de la création d'un **hook personnalisé avec React**. La dénomination de notre hook devient alors `useDebounce`, c'est la solution que je vous propose de passer en revue 👇

```
import React, { useRef, useEffect } from 'react';

export const useDebounce = (cb, delay, deps) => {
    const timeout = useRef();
    useEffect(() => {
        timeout.current = setTimeout(cb, delay);
        return () => clearTimeout(timeout.current);
    }, deps);
}
```

On retrouve ici la logique propre au debounce qui permet d'initialiser un timer depuis un `delay` dans l'optique de retourner une callback : le timer est initialisé lorsque le composant est monté et détruit lorsque le composant est démonté grâce à l'utilisation du hook `useEffect`.

Voyons à présent comment exploiter ce nouveau hook depuis notre blog :  

```
import React, {useState} from "react";

import {useDebounce} from "src/hooks/useDebounce"; // CUSTOM HOOK PATH

const Debounce = () => {
    const [searchValue, setSearchValue] = useState('');
    
    // Fetch articles with HTTP Request
    const fetchArticles = () => {
        console.log('fetchArticles() is called!');
    }
    
    /* Use of debounce hook to fetch articles */
    useDebounce(async () => {
        if (searchValue) fetchArticles();
        // else .. 
    }, 1000, [searchValue]); 

    /* Handle the value of search input */
    const handleSearchValue = (e) => {
        const newValue = e.target.value;
        setSearchValue(newValue);
    }
    
    return (
        <div>
            <input 
                type="text" 
                name="searchValue" 
                value={searchValue}
                onChange={handleSearchValue} 
                placeholder="Votre recherche ici.."
            />
            <br />
            {searchValue && (
                <p>Voici les articles correspondant à:<br/><b>« {searchValue} »</b></p>
                /* ... Display articles ... */
            )}
        </div>
    );
}

export default Debounce;
```

Notre debounce est enfin opérationnel 🎉

Vous l'avez certainement remarqué 🧐 : notre hook `useDebounce` a finalement l'allure d'un `useEffect`.

Seulement, il inclut à présent la logique et le comportement de notre fonction debounce.

La callback `handleSearchValue` est de ce fait réduite à sa seule et unique fonction : garantir la synchronicité de la valeur de notre barre de recherche avec l'état `searchValue`, alors que : parallèlement, notre debounce est à l'écoute des changements de cet état.

Le fetch des articles sera ainsi exécuté 1 seconde après la fin de saisie de l'utilisateur dans la barre de recherche (sachant que `delay` équivaut 1000ms), sous réserve que la recherche ne soit pas nulle.

> _Note : La callback transmise à notre hook `useDebounce` sera exécutée une première fois par défaut lorsque le composant `Debounce` sera monté. Cela peut représenter une contrainte dans certaines situations, une mesure conditionnelle devient nécessaire_.

---

## ⏯️ La fonction « Throttle »

La fonction « **Throttle** » permet de réguler le nombre d'exécution d'une fonction invoquée par une série d'évènements **en l'exécutant une seule fois** _(toutes les 1000ms par exemple)_ **selon un interval donné**.

Elle est souvent utilisée pour appréhender une **surexploitation des ressources** et d'éventuels **problèmes de performance** à travers un excès d'opérations. 

### Quand est-ce nécessaire ?

Reprenons l'exemple de notre blog et intéressons-nous à présent à la page de visualisation des articles : supposons que nous voulions implémenter un système de suivi des chapitres selon la position vertical du client par rapport à la hauteur de la page.

Nous aurions naturellement besoin des propriétés retournées par l'évènement `scroll` : 

```
useEffect(() => {
    /* Chapters following feature */
    const updateCurrentChapter = (scrollTop, scrollAvailable) => {
        console.log(`scroll progress => ${scrollTop}/${scrollAvailable}`); // debug
        // Feature implementation here..
    }
    /* Handle scroll */
    const handleScroll = (e) => {
        const scrollAvailable = (document.body.scrollHeight - window.innerHeight);
        const scrollTop = window.pageYOffset;
        updateCurrentChapter(scrollTop, scrollAvailable); // Throttle is required here
    }
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
}, []);
```

Si implémenter la nouvelle feature dans notre callback `handleScroll` semble idéal, elle peut pour autant impliquer de véritables problèmes de performance au fil de l'évolution de l'application.

En effet, de nouvelles fonctionnalités sont susceptibles d'être ajoutées et greffées à l'évènement `scroll`,  les opérations cumulées les unes avec les autres peuvent représenter une charge de calculs trop importante quant au nombre d'occurences excessif de l'évènement `scroll`.

C'est à cette occasion que le « **Throttle** » se révèle utile.

Il va permettre d'exécuter notre fonction `updateCurrentChapter(..)` et potentiellement d'autres fonctions nécessitant l'évènement `scroll`, au fil du déroulement de la page grâce à l'évènement `scroll`, à une fréquence plus raisonnable.

Si l'évènement `scroll` peut, dans cet exemple, invoquer notre fonction `updateCurrentChapter(..)` jusqu'à 18 fois par seconde, on pourrait dorénavant décider de ne l'exécuter qu'une seule fois en l'espace d'une seconde et ainsi déshériter notre naviguateur d'une charge de calculs inutile pour parvenir au même résultat.

### Comment ça fonctionne ?

**🟨 Approche globale en Javascript**

La fonction **Throttle** nécessite généralement deux paramètres:

- Une callback, nous utiliserons `cb` _(qui sera retourné sous condition)_
- Un delai, nous utiliserons `delay` _(en millisecondes)_

```
const throttle = (cb, delay) => {
    let wait = false;
    return (...args) => {
        if (wait) return;
        cb(...args);
        wait = true;
        setTimeout(() => {
            wait = false;
        }, delay);
    }
}
```

Lorsque notre fonction `throttle(..)` est invoquée, la callback `cb` fournie est instantanément retournée (sous réserve qu'elle n'ai pas été invoquée depuis `delay` millisecondes), il faudra à présent attendre `delay` millisecondes avant qu'elle puisse être retournée à nouveau.

Ainsi, le **throttle** permet de réduire le nombre d'occurence d'une fonction lorsqu'elle est parfois excessivement invoquée par une série d'évènements.

> _ℹ️ Note : La fréquence d'appels est réduite et on peut s'assurer que la fonction ne soit exécutée qu'une seule fois toutes les `delay` millisecondes_.

**⚙️ Hook React personnalisé <u>useThrottle</u>**

Oublions à présent notre blog et concentrons-nous sur le fonctionnement du throttle grâce à un bouton à cliquer. L'enjeu est simple : cliquer rapidement sur un bouton et incrémenter un compteur tout en l'empêchant de s'incrémenter plus d'une fois toutes les X secondes selon `delay`.

Le principe est le même que le cas précédent : **réguler l'exécution d'une fonction invoquée par une série d'évènements excessivement nombreux**.

Pour cela, commencons par créer notre nouveau hook React `useThrottle`. 

```
import { useEffect, useRef, useState } from 'react';

export const useThrottle = (value, delay = 500) => {
    const [throttledValue, setThrottledValue] = useState(value);
    const lastExecuted = useRef(Date.now());
    
    useEffect(() => {
        if (Date.now() >= lastExecuted.current + delay) {
            lastExecuted.current = Date.now();
            setThrottledValue(value);
        } else {
            /* Last update */
            const timeout = setTimeout(() => {
                lastExecuted.current = Date.now();
                setThrottledValue(value);
            }, delay);
            return () => clearTimeout(timeout);
        }
    }, [value, delay]);

    return throttledValue;
}
```


Vous l'avez remarqué, notre callback `cb` auparavant utilisée a été délaissée au profit d'une valeur `value`. Quelque soit la fréquence à laquelle `value` est modifiée, notre throttle garantie de l'actualiser et la retourner mais ne l'actualisera pas plus d'une fois chaque `delay` millisecondes.

Une fois le composant monté, l'object `lastExecuted` devient persistant : on lui attribuera une date `Date.now()` grâce à laquelle nous déterminerons si la valeur `value` doit être modifiée et si la date `lastExecuted.current` doit être raffraichit.

Lorsque `value` change, le throttle est sollicité. Si elle n'a pas été changée durant les `delay` millisecondes, on raffraichit la valeur retournée et actualisons la date `lastExecuted.current`, le cas échéant nous lançons un timer pour une dernière exécution dans l'hypothèse où aucun nouvel appel ne vienne l'annuler.

Implémentons à présent notre bouton et nos compteurs : 

```
import React, { useState, useEffect } from 'react';

import { useThrottle } from 'src/hooks/useThrottle';

const Throttle = () => {
    const [clickCount, setClickCount] = useState(0);
    const [clickCountThrottle, setClickCountThrottle] = useState(0);

    const throttledValue = useThrottle(clickCount, 1000);
    useEffect(() => {
        if (throttledValue) setClickCountThrottle(clickCountThrottle + 1);
    }, [throttledValue]);

    return (
        <div className="Container">
            <h2>Throttle</h2>
            <div className="Content">
                <button onClick={() => setClickCount(clickCount + 1)}>Cliquez-moi</button>
            </div>
            <div className="Content">
                <div className="Result_Item">
                    <i>Nb. de click (sans throttle)</i><br />
                    <span>{clickCount}</span>
                </div>
                <div className="Result_Item">
                    <i>Nb. de click (avec throttle 1000ms)</i><br />
                    <span>{clickCountThrottle}</span>
                </div>
            </div>
        </div>
    )
}

export default Throttle;
```

Et voilà 🎉

Si l'on clique 11 fois sur notre bouton en moins de 2 secondes, notre état `clickCountThrottle` ne sera incrémenté que 2 fois. `clickCount` en revanche sera égal à 11.

La valeur `throttledValue` utilisant notre fonction `throttle(..)` et basé sur la valeur de `clickCount` peut à présent être utilisée et devenir une dépendance pour ainsi permettre l'exécution d'une ou plusieurs fonctions grâce au hook `useEffect`.

---

## Liens et sources

Les snippets de code inclus dans cet articles peuvent être retrouvés depuis une source accessible depuis un repo. Github (voir ci-dessous).

Les exemples peuvent être simulés via une démo live hébergé par **CodeSandbox.io** _(lien ci-dessous)_, ainsi, le code incluant les différentes fonctions étudiées est accessible à tou.s.tes.

**Github Repository** _(Source)_
[https://github.com/MessageGit/debounce-and-throttle-react-hooks](https://github.com/MessageGit/debounce-and-throttle-react-hooks)

**Live Demo** _(CodeSandbox.io)_
[https://licvw6-3000.csb.app/](https://licvw6-3000.csb.app/)

**Lodash** _(JS Lib. including **Debounce** and **Throttle** functions)_
[https://lodash.com/](https://lodash.com/)