Retour au blog
août 2026·4 min de lectureGénéré par IA
Langue:DeutschEnglishFrançaisÖsterreichischKlingonian Version originale: English

Debouncing et throttling : dompter les événements à haute fréquence en JavaScript

#javascript#performance#frontend

Tout développeur frontend finit par se heurter au même mur : un gestionnaire de scroll qui se déclenche des centaines de fois par seconde, un champ de recherche qui lance une requête réseau à chaque frappe, ou un écouteur de redimensionnement qui recalcule la mise en page si souvent que la page se met à saccader. Le debouncing et le throttling sont les deux techniques qui règlent cela, et une fois la différence comprise, choisir la bonne devient automatique.

Le problème : les événements arrivent plus vite que vous ne pouvez les traiter

Les navigateurs ne limitent pas pour vous le débit des événements du DOM. scroll, resize, mousemove et input peuvent tous se déclencher des dizaines de fois par seconde. Si le gestionnaire attaché à l'un d'eux fait quoi que ce soit de non trivial (un fetch, une mesure du DOM, un nouveau rendu), vous effectuez ce travail coûteux bien plus souvent que l'utilisateur n'en a réellement besoin.

window.addEventListener('resize', () => {
  recalculateLayout(); // runs on every single pixel of resize
});

Le debouncing et le throttling réduisent tous deux la fréquence d'exécution effective du gestionnaire, mais avec des garanties différentes.

Debounce : attendre le silence

Le debouncing retarde l'exécution jusqu'à ce que les événements s'arrêtent pendant une durée donnée. Chaque nouvel appel réinitialise le minuteur. C'est le bon outil quand seul l'état final après une rafale d'activité compte : la valeur d'un champ de recherche une fois que l'utilisateur a cessé de taper, et non chaque frappe intermédiaire.

function debounce(fn, delayMs) {
  let timeoutId;
  return function debounced(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn.apply(this, args), delayMs);
  };
}

Utilisation : un champ de recherche en direct qui ne doit pas solliciter le réseau à chaque frappe.

const searchInput = document.querySelector('#search');

const runSearch = debounce((event) => {
  fetchResults(event.target.value);
}, 300);

searchInput.addEventListener('input', runSearch);

Tapez « angular » et fetchResults se déclenche une seule fois, 300 ms après la dernière frappe, et non sept fois.

Throttle : garantir un rythme régulier

Le throttling plafonne l'exécution à une fois par intervalle au maximum, quel que soit le nombre d'événements reçus pendant cette fenêtre. C'est le bon outil pour les flux continus où il faut des mises à jour périodiques pendant toute la durée, et pas seulement à la fin : un suivi de position de scroll, un gestionnaire de glisser-déposer, un effet de curseur piloté par mousemove.

function throttle(fn, intervalMs) {
  let lastCall = 0;
  let timeoutId = null;

  return function throttled(...args) {
    const now = Date.now();
    const remaining = intervalMs - (now - lastCall);

    if (remaining <= 0) {
      clearTimeout(timeoutId);
      timeoutId = null;
      lastCall = now;
      fn.apply(this, args);
    } else if (!timeoutId) {
      timeoutId = setTimeout(() => {
        lastCall = Date.now();
        timeoutId = null;
        fn.apply(this, args);
      }, remaining);
    }
  };
}

Utilisation : recalculer la mise en page au redimensionnement, au plus toutes les 200 ms.

window.addEventListener(
  'resize',
  throttle(() => {
    recalculateLayout();
  }, 200),
);

Notez la branche else if (!timeoutId) : elle planifie un appel final pour que le dernier événement de redimensionnement d'une rafale soit quand même traité, au lieu d'être silencieusement ignoré parce qu'il est tombé dans la fenêtre de pause.

Choisir entre les deux

  • Debounce quand seul l'état final compte : champ de recherche, validation de formulaire, sauvegarde automatique, gestionnaires de redimensionnement qui ne doivent s'exécuter qu'une fois la stabilisation terminée.
  • Throttle quand il faut un retour continu pendant l'activité : déclencheurs de défilement infini, barres de progression liées à la position de scroll, glisser-déposer, suivi de curseur en direct.

Règle pratique : le debounce répond à « qu'ont-ils fini par faire ? » ; le throttle répond à « que font-ils en ce moment, échantillonné régulièrement ? ».

Pièges courants

  • Perdre this. Les deux implémentations ci-dessus utilisent fn.apply(this, args), donc elles fonctionnent correctement comme méthodes d'objet ou gestionnaires d'événements liés normalement. Si vous enveloppez une fonction fléchée qui a déjà capturé le mauvais this, aucun .apply() n'y changera rien : l'erreur s'est produite plus tôt.
  • Ne pas nettoyer au démontage. Dans un framework à composants (React, Angular, Vue), un gestionnaire avec debounce ou throttle peut encore être en attente dans un setTimeout après la disparition du composant qui l'a créé, et son déclenchement peut alors planter ou écrire dans un état périmé. Annulez-le : conservez une référence à timeoutId et effacez-la dans le chemin de nettoyage (ngOnDestroy, fonction de nettoyage de useEffect, etc.).
  • Délai de debounce trop long. 300 ms est une valeur courante pour les champs de recherche, mais pour quelque chose comme la validation de formulaire en ligne, même 300 ms peuvent paraître lents. Ajustez le délai à l'interaction, pas à un nombre copié depuis un article de blog.
  • Throttle sans appel final. Un throttle naïf qui vérifie seulement « assez de temps s'est-il écoulé ? » et ne fait rien sinon ignorera silencieusement le dernier événement d'une rafale. Si le dernier état compte (et c'est généralement le cas), veillez à ce que votre implémentation planifie un appel final comme celle ci-dessus.

Faut-il écrire le sien ?

Les implémentations présentées ici sont volontairement minimales pour montrer le vrai mécanisme : des fermetures qui conservent l'état du minuteur entre les appels. En production, recourez plutôt à un utilitaire maintenu : debounce et throttle de Lodash gèrent des cas limites comme la configuration du front montant ou descendant et les méthodes .cancel()/.flush(), faciles à mal écrire à la main. Écrire le sien est un bon exercice pour comprendre ce qui se passe sous le capot ; recourir à une version éprouvée est le bon choix dès que cela compte.

À retenir

Le debounce condense une rafale d'événements en un seul appel une fois celle-ci retombée. Le throttle répartit les appels uniformément pendant la rafale. Les deux existent pour la même raison : le navigateur ne sait pas à quelle vitesse votre code peut se permettre de s'exécuter, c'est donc à vous de le lui dire.

Plus récentComprendre les Web Workers pour les calculs lourdsPlus ancienÉcrire un outil en ligne de commande simple avec Node.js