W3docs

JavaScript Debounce et Throttle

Apprenez à limiter la fréquence d'exécution des fonctions JavaScript avec debounce et throttle — différences, implémentation et cas d'usage.

Certains événements se déclenchent bien plus souvent qu'il n'est utile d'y répondre. Taper dans une zone de recherche déclenche un événement input à chaque frappe ; faire défiler une page peut émettre des centaines d'événements scroll par seconde ; resize et mousemove sont tout aussi bavards. Si chaque événement exécute un traitement coûteux — une requête réseau, un calcul de mise en page, un re-rendu — votre application se met à ramer. Debounce et throttle sont deux petits wrappers qui plafonnent la fréquence d'exécution d'une fonction, maintenant une réactivité élevée sans modifier ce que fait la fonction.

Les deux sont des patterns décorateurs classiques : ils prennent une fonction et en retournent une nouvelle avec le même comportement, augmenté d'une règle de limitation de fréquence. Ils reposent sur les closures pour mémoriser l'état entre les appels, et sur des timers comme setTimeout pour différer ou contrôler l'exécution.

Le concept central

Les deux techniques répondent à la même question — « à quelle fréquence ceci doit-il s'exécuter ? » — de façon opposée :

  • Debounce attend une pause. Il reporte l'appel jusqu'à ce que N millisecondes se soient écoulées depuis la dernière invocation. Si les appels continuent d'affluer, le timer se réinitialise en permanence et la fonction ne se déclenche jamais. À retenir : « attendre le silence. »
  • Throttle impose une cadence régulière. Il laisse la fonction s'exécuter au plus une fois toutes les N millisecondes, quel que soit le nombre d'appels intermédiaires. À retenir : « battement de cœur régulier. »
AspectDebounceThrottle
Se déclenche quandL'activité s'arrête pendant N msAu plus une fois toutes les N ms
Pendant une rafaleRien ne s'exécute jusqu'à la fin de la rafaleS'exécute selon un calendrier fixe
Modèle mental« Attendre le silence »« Cadence régulière »
Adapté pourRecherche en cours de frappe, sauvegarde automatique, fin de redimensionnementSuivi du défilement, glisser-déposer, défilement infini

Debounce

Une fonction debounce annule tout timer en attente à chaque appel et en planifie un nouveau. Ce n'est que lorsque les appels cessent enfin pendant delay millisecondes que la fonction encapsulée s'exécute réellement.

function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

Deux détails rendent cette implémentation robuste. Le wrapper collecte tous les arguments avec les paramètres rest (...args) et les transmet, de sorte que la fonction encapsulée reçoit exactement ce que l'appelant a passé. Et elle invoque fn avec fn.apply(this, args) afin de préserver le this d'origine — important lorsque la fonction debounce est une méthode sur un objet. (Voir call et apply et la liaison de fonctions pour comprendre pourquoi transmettre this est essentiel.)

Voici comment cela fonctionne en pratique. Appeler la fonction encapsulée à plusieurs reprises ne provoque qu'une seule exécution réelle, une fois l'activité calmée :

javascript— editable

Parce que chaque frappe réinitialise le compteur, debounce est idéal chaque fois que vous souhaitez réagir après que l'utilisateur a terminé : recherche en cours de frappe, sauvegarde automatique d'un brouillon, validation d'un champ une fois la saisie terminée, ou recalcul d'une mise en page uniquement lorsqu'un redimensionnement de fenêtre s'est stabilisé.

Throttle

Une fonction throttle s'exécute immédiatement, puis ignore les appels suivants jusqu'à l'expiration d'un délai de refroidissement. Cela garantit une fréquence maximale plutôt que d'attendre une pause.

function throttle(fn, limit) {
  let inThrottle = false;
  return function (...args) {
    if (!inThrottle) {
      fn.apply(this, args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

L'indicateur inThrottle, conservé dans la closure, agit comme un verrou. Le premier appel passe et le verrou se ferme ; tous les appels pendant le délai de refroidissement sont ignorés ; lorsque le timer se déclenche, le verrou se rouvre pour le prochain appel.

javascript— editable

Throttle convient à tout ce qui diffuse en continu et pour lequel vous souhaitez des mises à jour régulières plutôt que chaque occurrence : suivi de la position de défilement, gestion du mousemove lors d'un glisser-déposer, chargement de contenu supplémentaire en défilement infini, ou limitation de la fréquence des appels à une API soumise à des quotas.

Déclenchement en début ou en fin d'intervalle

Il existe un choix de conception subtil dans les deux wrappers : la fonction doit-elle se déclencher sur le front montant (premier appel, immédiatement) ou sur le front descendant (après le délai/refroidissement) ?

  • Le debounce ci-dessus est en fin d'intervalle : rien ne se passe jusqu'à l'arrêt de l'activité. Un debounce en début d'intervalle s'exécuterait au premier appel, puis ignorerait les suivants.
  • Le throttle ci-dessus est en début d'intervalle : il se déclenche immédiatement puis impose un verrou. Un throttle en fin d'intervalle s'exécuterait également une dernière fois à la fin de la fenêtre pour capturer la valeur finale.

Ces comportements ont une importance pratique — un throttle en fin d'intervalle sur le défilement, par exemple, garantit que vous ne manquez pas la position finale lorsque l'utilisateur s'arrête.

Info

Pour du code en production, privilégiez une implémentation éprouvée comme _.debounce et _.throttle de lodash. Elles gèrent les déclenchements en début et en fin d'intervalle, une API cancel()/flush(), et une option maxWait (pour qu'une fonction debounce finisse toujours par s'exécuter lors d'une activité continue). Comprendre les versions de base ci-dessus est essentiel, mais vous avez rarement besoin de produire la vôtre.

Un exemple concret avec le DOM

Connecter debounce à un champ de recherche est le cas d'usage canonique. Nous attachons un seul écouteur (voir la gestion des événements dans le DOM) et laissons le wrapper décider du moment où le traitement s'exécute réellement :

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

function search(event) {
  console.log('Querying API for:', event.target.value);
  // fetch(`/api/search?q=${event.target.value}`) ...
}

const debouncedSearch = debounce(search, 400);

input.addEventListener('input', debouncedSearch);

Désormais, la requête réseau ne se déclenche que lorsque l'utilisateur fait une pause de 400 ms, au lieu de se produire à chaque frappe — une zone de recherche qui déclenchait auparavant une dizaine de requêtes pour hello n'en déclenche plus qu'une. Notez que l'écouteur reçoit l'objet event du DOM et, comme notre wrapper transmet chaque argument, search le reçoit intact.

Avertissement

Les timers et les écouteurs conservent des références, il faut donc les nettoyer lorsqu'ils ne sont plus nécessaires. Dans une application à page unique ou un composant, supprimez l'écouteur au démontage (par exemple, lors du démontage du composant) et annulez tout timer en attente pour éviter les fuites mémoire et les callbacks qui se déclenchent sur des éléments qui n'existent plus :

input.removeEventListener('input', debouncedSearch);

Un debounce en production expose généralement aussi une méthode cancel() qui appelle clearTimeout pour vous.

Choisir entre les deux

Lorsque vous ne savez pas lequel utiliser, demandez-vous ce qui compte pour vous :

  • Vous n'avez besoin que de l'état final après une rafale d'activité (le terme de recherche terminé, la taille de fenêtre stabilisée) ? Utilisez debounce.
  • Vous voulez un retour continu et régulier pendant l'activité (progression du défilement, position d'un élément en cours de déplacement) ? Utilisez throttle.

Les deux sont légers, indépendants du framework, et reposent directement sur les closures et les timers — les mêmes fondements que les fonctions fléchées capturant this et les outils de planification que vous avez déjà vus.

Testez vos connaissances

Pratique
Quelle technique déclenche la fonction au plus une fois par intervalle de temps fixe, quel que soit le nombre d'appels ?
Quelle technique déclenche la fonction au plus une fois par intervalle de temps fixe, quel que soit le nombre d'appels ?
Pratique
Vous souhaitez envoyer une requête de recherche uniquement après que l'utilisateur a arrêté de taper. Quel est le bon outil ?
Vous souhaitez envoyer une requête de recherche uniquement après que l'utilisateur a arrêté de taper. Quel est le bon outil ?
Pratique
Pourquoi les exemples de wrappers debounce et throttle appellent-ils la fonction d'origine avec `fn.apply(this, args)` plutôt que simplement `fn(args)` ?
Pourquoi les exemples de wrappers debounce et throttle appellent-ils la fonction d'origine avec `fn.apply(this, args)` plutôt que simplement `fn(args)` ?
Was this page helpful?