DEV Community

Cover image for ⏰ Les fonctions Debounce et Throttle : comment ça fonctionne ?
Grégory CHEVALLIER
Grégory CHEVALLIER

Posted on

⏰ Les fonctions Debounce et Throttle : comment ça fonctionne ?

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;
Enter fullscreen mode Exit fullscreen mode

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.

Résultat de la recherche : 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);
    };
};
Enter fullscreen mode Exit fullscreen mode

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, la plus connue.

⚙️ Hook React personnalisé useDebounce

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
}, []);
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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é useThrottle

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

Live Demo (CodeSandbox.io)
https://licvw6-3000.csb.app/

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

Top comments (0)