Retour au cours

frontend / javascript

Debounce et throttle

Leçon 181 exercice

Explication

Ce que vous allez apprendre

  • Expliquer pourquoi certains événements DOM nécessitent de limiter la fréquence de réaction
  • Implémenter un debounce à partir de setTimeout et d'une closure
  • Implémenter un throttle garantissant un rythme d'exécution régulier
  • Choisir entre debounce et throttle selon le cas d'usage (recherche, scroll, redimensionnement)
  • Éviter la fuite de timer la plus fréquente lors de l'implémentation d'un debounce

Dans quel contexte ?

Un champ de recherche envoie une requête à l'API à chaque frappe clavier : taper "ordinateur" (10 lettres) déclenche 10 requêtes réseau, dont 9 sont immédiatement obsolètes. Le serveur se retrouve surchargé de requêtes inutiles, et l'utilisateur voit les résultats clignoter avant de se stabiliser. La correction standard consiste à debouncer l'appel : attendre 300ms d'inactivité avant de lancer réellement la requête, ce qui réduit les 10 appels réseau à un seul dans la majorité des cas.

1. Le problème : des événements trop fréquents

Certains événements du navigateur — taper au clavier, faire défiler la page, déplacer la souris — se déclenchent des dizaines, voire des centaines de fois par seconde. Réagir à CHACUN, en lançant par exemple une requête réseau à chaque frappe, surcharge inutilement le serveur.

Prérequis

Cette leçon s'appuie directement sur les closures (leçon 2) : debounce et throttle fonctionnent tous deux en conservant un état (un identifiant de timer) capturé dans une closure entre deux appels.

2. Une première solution : le debounce

Le debounce retarde l'exécution jusqu'à ce qu'une période d'inactivité soit constatée : chaque nouvel appel annule et reprogramme le précédent. C'est le choix naturel pour un champ de recherche.

3. Pourquoi le debounce convient à la recherche

On ne veut lancer la requête QUE lorsque l'utilisateur a fini de taper, pas à chaque lettre. Le debounce garantit exactement ce comportement, grâce aux closures et à setTimeout déjà vus plus tôt dans le cours.

4. Une seconde solution : le throttle

Le throttle, à l'inverse, garantit un rythme régulier d'exécution, au plus une fois par intervalle donné, sans jamais attendre une accalmie complète.

5. Pourquoi le throttle convient au scroll

C'est le choix adapté pour un événement de défilement, où l'on veut réagir en continu, mais pas plus souvent que nécessaire pour rester fluide — contrairement au debounce, qui attendrait la fin du scroll.

6. La question à se poser pour choisir

Le debounce répond à "l'utilisateur a-t-il fini ?", le throttle répond à "à quelle fréquence maximale dois-je réagir ?". Ces deux fonctions, bien qu'apparemment simples, cachent des subtilités d'implémentation qui reviennent souvent en entretien technique.

TechniqueComportementCas d'usage typique
DebounceAttend une accalmie avant d'exécuterChamp de recherche, validation de formulaire
ThrottleExécute au plus une fois par intervalleScroll, redimensionnement de fenêtre

Piège fréquent

Oublier clearTimeout(timerId) avant de reprogrammer un nouveau setTimeout dans un debounce fait perdre son effet à la fonction : chaque appel programme un nouvel exécution sans annuler la précédente, et la fonction finit par s'exécuter plusieurs fois au lieu d'une seule.

Commandes & code

Debounce et throttle

Maîtriser la fréquence d'exécution face à des événements haute fréquence.

js
// --- Debounce : n'execute la fonction qu'apres une PERIODE D'INACTIVITE ---
// Usage typique : champ de recherche, redimensionnement de fenetre, validation de formulaire
function debounce(fonction, delai) {
    let timeoutId;
    return function (...args) {
        clearTimeout(timeoutId);                 // annule l'appel precedent en attente
        timeoutId = setTimeout(() => fonction.apply(this, args), delai);
    };
}

const rechercherDebounced = debounce((terme) => {
    console.log(`Recherche lancee pour : "${terme}"`);
    // appel API reel ici
}, 300);

// Simule une saisie rapide au clavier : seul le DERNIER appel (apres 300ms d'inactivite) s'execute
document.querySelector("#recherche")?.addEventListener("input", (e) => {
    rechercherDebounced(e.target.value);
});

// --- Throttle : limite l'execution a AU PLUS une fois par intervalle regulier ---
// Usage typique : scroll, mousemove, resize -- garantir un flux constant, pas juste la fin
function throttle(fonction, intervalle) {
    let enAttente = false;
    let dernierArgs = null;
    return function (...args) {
        dernierArgs = args;
        if (!enAttente) {
            fonction.apply(this, dernierArgs);
            enAttente = true;
            setTimeout(() => {
                enAttente = false;
            }, intervalle);
        }
    };
}

const gererScrollThrottled = throttle(() => {
    console.log(`Position de scroll : ${window.scrollY}`);
}, 200);

window.addEventListener("scroll", gererScrollThrottled);

// --- Version throttle "leading + trailing" : execute au debut ET a la fin de la periode ---
function throttleAvance(fonction, intervalle) {
    let dernierAppel = 0;
    let timeoutId = null;
    return function (...args) {
        const maintenant = Date.now();
        const tempsRestant = intervalle - (maintenant - dernierAppel);
        if (tempsRestant <= 0) {
            clearTimeout(timeoutId);
            dernierAppel = maintenant;
            fonction.apply(this, args);
        } else if (!timeoutId) {
            timeoutId = setTimeout(() => {
                dernierAppel = Date.now();
                timeoutId = null;
                fonction.apply(this, args);
            }, tempsRestant);
        }
    };
}

// --- Debounce avec execution immediate (leading edge) ---
function debounceImmediat(fonction, delai) {
    let timeoutId;
    return function (...args) {
        const appelImmediat = !timeoutId;
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
            timeoutId = null;
        }, delai);
        if (appelImmediat) fonction.apply(this, args);   // execute au PREMIER appel, pas au dernier
    };
}

// --- Debounce annulable (pattern utile en production) ---
function debounceAnnulable(fonction, delai) {
    let timeoutId;
    const debounced = function (...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => fonction.apply(this, args), delai);
    };
    debounced.annuler = () => clearTimeout(timeoutId);
    return debounced;
}

const sauvegardeAuto = debounceAnnulable(() => console.log("Sauvegarde automatique"), 1000);
sauvegardeAuto();
sauvegardeAuto.annuler();       // annule la sauvegarde en attente (ex: si l'utilisateur quitte la page)

// --- requestAnimationFrame : throttle naturel pour les animations (aligne sur le rafraichissement ecran) ---
function gererMouseMoveOptimise() {
    let ticking = false;
    window.addEventListener("mousemove", (event) => {
        if (!ticking) {
            requestAnimationFrame(() => {
                console.log(event.clientX, event.clientY);
                ticking = false;
            });
            ticking = true;
        }
    });
}

Résumé

  • Le debounce attend une accalmie avant d'exécuter (recherche, validation) ; le throttle garantit un rythme régulier (scroll, resize).
  • Une implémentation debounce simple exécute à la FIN de la période d'inactivité ("trailing").
  • requestAnimationFrame est un throttle naturel, aligné sur le taux de rafraîchissement de l'écran.
  • Une fonction debounce "annulable" évite d'exécuter une action différée devenue obsolète.

Exercices pratiques

1 disponible
1

Mission : la recherche qui envoie une requête par lettre tapée

Objectif : Choisir et implémenter debounce vs throttle selon deux scénarios distincts, et corriger un bug d'oubli de clearTimeout.

Contexte

Sur le site de Technologik, un champ de recherche envoie une requête API à CHAQUE lettre tapée, saturant le serveur. Un développeur a tenté de corriger avec ce code, mais le bug persiste :

js
function debounceCasse(fonction, delai) {
  let timeoutId;
  return function (...args) {
    timeoutId = setTimeout(() => fonction.apply(this, args), delai);
  };
}

Par ailleurs, un gestionnaire de scroll qui recalcule la position d'une barre de progression doit réagir en continu pendant le défilement, pas seulement à la fin.

Résoudre l’exercice →