Debounce et throttle en JavaScript : enfin comprendre la différence

Un champ de recherche qui interroge l'API à chaque frappe, un gestionnaire scroll déclenché soixante fois par seconde : certains événements arrivent bien trop souvent. Debounce et throttle limitent les appels, mais pas de la même façon.

Debounce : attendre que ça se calme

Le debounce repousse l'appel tant que les événements continuent d'arriver. La fonction ne s'exécute qu'une fois, après un temps de calme :

function debounce(fn, delai = 300) {
  let minuteur;
  return function (...args) {
    clearTimeout(minuteur);
    minuteur = setTimeout(() => fn.apply(this, args), delai);
  };
}

const champ = document.querySelector('#recherche');
champ.addEventListener(
  'input',
  debounce(async (event) => {
    const q = encodeURIComponent(event.target.value);
    const reponse = await fetch(`/api/recherche?q=${q}`);
    afficherResultats(await reponse.json());
  }, 250),
);

Throttle : au maximum une fois par intervalle

Le throttle, lui, garantit un rythme régulier : la fonction s'exécute au plus une fois toutes les n millisecondes, même si les événements continuent.

function throttle(fn, intervalle = 100) {
  let dernierAppel = 0;
  return function (...args) {
    const maintenant = Date.now();
    if (maintenant - dernierAppel >= intervalle) {
      dernierAppel = maintenant;
      fn.apply(this, args);
    }
  };
}

window.addEventListener(
  'scroll',
  throttle(() => {
    const progression = window.scrollY / (document.body.scrollHeight - innerHeight);
    barre.style.transform = `scaleX(${progression})`;
  }, 50),
  { passive: true },
);

La version TypeScript

Avec un générique sur les arguments, la fonction renvoyée garde la signature de la fonction d'origine :

export function debounce<A extends unknown[]>(
  fn: (...args: A) => void,
  delai = 300,
): (...args: A) => void {
  let minuteur: ReturnType<typeof setTimeout> | undefined;
  return (...args: A) => {
    clearTimeout(minuteur);
    minuteur = setTimeout(() => fn(...args), delai);
  };
}

const sauvegarder = debounce((id: number, contenu: string) => {
  void api.enregistrer(id, contenu);
}, 1000);

sauvegarder(42, 'brouillon'); // ✔ typé

Lequel choisir ?

SituationTechniquePourquoi
Recherche pendant la frappeDebounceSeule la saisie finale compte
Sauvegarde automatiqueDebounceOn enregistre quand l'utilisateur fait une pause
Barre de progression au défilementThrottleIl faut des mises à jour régulières
Redimensionnement de fenêtreLes deuxThrottle pour l'aperçu, debounce pour le calcul final

Pour les animations liées au défilement, requestAnimationFrame est souvent encore mieux qu'un throttle : il s'aligne sur le rafraîchissement de l'écran.