Le debounce en JavaScript : principe et implémentation

Lors d'interactions sur une page web, certains événements peuvent être déclenchés de manière très répétée, ce qui sollicite inutilement les ressources du navigateur. Prenons l'exemple d'un bouton permettant de réclamer un coupon promotionnel : un utilisateur impatient peut cliquer des dizaines de fois en quelques secondes, alors qu'une seule exécution suffit pour traiter la demande.

Le debounce consiste à regrouper ces déclenchements succcessifs en n'en conservant qu'un seul, exécuté après que l'activité cesse pendant une durée définie.

Cas d'usage typiques : validation de formulaire, recherche avec auto-complétion, redimensionnement de fenêtre, événements de défilement.

Mise en évidence du problème

Considérons un simple bouton dont on souhaite détecter le clic :


<html lang="fr">
<head>
  <meta charset="UTF-8">
  <title>Débounce</title>
</head>
<body>
  <button id="monBouton">Cliquez ici</button>
  <script src="app.js"></script>
</body>
</html>

const btn = document.querySelector('#monBouton');

const gererClic = () => {
  console.log('Bouton activé');
};

btn.addEventListener('click', gererClic);

Sans dispositif particulier, chaque clic génère un appel immédiat. En cas de clics rapprochés, on obtient autant de logs que de clics, ce qui est superflu.

Première approche : debounce manuel

L'idée consiste à différer l'exécution : à chaque clic, on annule le retardement précédent et on en planifie un nouveau. Ainsi, seul le dernier clic déclenche réellement l'action, après un délai d'inactivité.

const btn = document.querySelector('#monBouton');
let minuterie = null;

const gererClic = () => {
  // On annule le retardement en cours s'il existe
  if (minuterie) {
    clearTimeout(minuterie);
  }
  // On planifie une nouvelle exécution différée
  minuterie = setTimeout(() => {
    console.log('Bouton activé');
    minuterie = null;
  }, 500);
};

btn.addEventListener('click', gererClic);

La variable minuterie doit impérativement vivre en dehors de la fonction de rappel, afin de conserver la référence au retardement précédent entre deux appels. Si elle était déclarée à l'intérieur, chaque clic créerait un nouveau minuteur inaccessible, et tous les logs finiraient par s'exécuter sans annulation.

Avec cette logique, les clics répétés n'engendrent aucun log tant que l'utilisateur continue de cliquer. Ce n'est qu'après ~500 ms d'inactivité que l'action se déclenche une seule fois.

Création d'un utilitaire réutilisable

Pour éviter de dupliquer cette logique à chaque fois, on peut encapsuler le mécanisme dans une fonction dédiée. Celle-ci prend en paramètres la fonction à exécuter et le délai d'attente, puis renvoie une version « debouncée » de cette fonction.

/**
 * Crée une version debouncée d'une fonction.
 * @param {Function} action - La fonction à exécuter
 * @param {number} delai - Délai d'attente en millisecondes
 * @returns {Function} - Fonction enveloppée avec le debounce
 */
function creerDebounce(action, delai) {
  let idRetardement = null;

  return function (...args) {
    // Annuler le retardement précédent
    if (idRetardement !== null) {
      clearTimeout(idRetardement);
    }
    // Planifier la nouvelle exécution avec son contexte
    idRetardement = setTimeout(() => {
      action.apply(this, args);
      idRetardement = null;
    }, delai);
  };
}

Le recours aux closures permet de préserver idRetardement entre les invocations successives de la fonction retournée. Chaque appel annule et remplace le minuteur précédent, garantissant que seule la dernière occurrence finit par s'exécuter. L'utilisation de apply et du spread ...args assure la transmission correcte du contexte this et des arguments.

Application au défilement de page

L'événement scroll est particulièrement gourmand : un simple mouvement de molette peut générer des dizaines d'événements par seconde.


<html lang="fr">
<head>
  <meta charset="UTF-8">
  <title>Débounce - Scroll</title>
</head>
<body style="height: 250vh;">
  <p>Faites défiler la page</p>
  <script src="app.js"></script>
</body>
</html>

Sans debounce, l'affichage de la position de défilement sature la console :

window.addEventListener('scroll', () => {
  console.log('Position :', window.scrollY);
});

Avec l'utilitaire, la sortie est nettement maîtrisée :

const afficherPosition = creerDebounce(() => {
  console.log('Position :', window.scrollY);
}, 500);

window.addEventListener('scroll', afficherPosition);

Désormais, le log n'apparaît que 500 ms après la fin du défilement actif, divisant drastiquement le nombre d'appels et économisant les ressources du navigateur.

Code complet


<html lang="fr">
<head>
  <meta charset="UTF-8">
  <title>Débounce - Démonstration</title>
</head>
<body style="height: 250vh;">
  <button id="monBouton">Cliquez ici</button>
  <script src="app.js"></script>
</body>
</html>

// Fonction utilitaire de debounce
function creerDebounce(action, delai) {
  let idRetardement = null;

  return function (...args) {
    if (idRetardement !== null) {
      clearTimeout(idRetardement);
    }
    idRetardement = setTimeout(() => {
      action.apply(this, args);
      idRetardement = null;
    }, delai);
  };
}

// --- Démo 1 : clic sur bouton ---
const btn = document.querySelector('#monBouton');

btn.addEventListener('click', creerDebounce(() => {
  console.log('Bouton activé');
}, 500));

// --- Démo 2 : défilement de page ---
const afficherPosition = creerDebounce(() => {
  console.log('Position :', window.scrollY);
}, 500);

window.addEventListener('scroll', afficherPosition);

Étiquettes: JavaScript Debounce closures setTimeout optimisation de performances

Publié le 9 juillet à 03h33