À propos de ce cours
Découvrez comment optimiser les événements qui se déclenchent en rafale (input, scroll, resize, mousemove) grâce aux techniques debounce et throttle. La leçon explique leur fonctionnement, montre comment les écrire en JavaScript, quand choisir l'une ou l'autre, et présente les alternatives modernes : requestAnimationFrame, écouteurs passifs et IntersectionObserver. Avec des exemples mesurés dans la console et des activités.
-
Objectifs
- Identifier les événements qui se déclenchent en rafale (
input,scroll,resize,mousemove). - Comprendre et écrire une fonction debounce.
- Comprendre et écrire une fonction throttle.
- Choisir la bonne technique et connaître les alternatives modernes (
requestAnimationFrame, écouteurs passifs,IntersectionObserver).
- Identifier les événements qui se déclenchent en rafale (
-
Le problème : les événements en rafale
- Certains événements se déclenchent des dizaines de fois par seconde:
input: à chaque caractère tapé dans un champ ;scroll: en continu pendant le défilement ;resize: pendant tout le redimensionnement de la fenêtre ;mousemove: à chaque déplacement de la souris.
- Si chaque déclenchement lance un traitement lourd (requête au serveur, calcul, modification du DOM), la page ralentit, le serveur reçoit des requêtes inutiles et la batterie s'use.
// ✘ Une requête au serveur à CHAQUE touche : « bootstrap » = 9 requêtes champRecherche.addEventListener("input", () => { rechercher(champRecherche.value); });
- Certains événements se déclenchent des dizaines de fois par seconde:
-
Debounce : attendre que l'utilisateur s'arrête
- Le debounce (anti-rebond) retarde l'exécution : la fonction ne s'exécute que lorsque l'événement a cessé depuis un certain délai. Chaque nouvel événement relance le compte à rebours.
- Image : un ascenseur qui attend que plus personne n'entre pendant 3 secondes avant de fermer ses portes.
function debounce(fonction, delai = 300) { let minuteur; return function (...args) { clearTimeout(minuteur); // annule le compte précédent minuteur = setTimeout(() => fonction.apply(this, args), delai); // relance }; } // ✔ Une seule recherche, 400 ms après la dernière touche const rechercherAvecDelai = debounce((texte) => rechercher(texte), 400); champRecherche.addEventListener("input", () => rechercherAvecDelai(champRecherche.value));- Comment ça marche :
debouncerenvoie une nouvelle fonction qui garde en mémoire la variableminuteur(c'est une closure). À chaque appel, l'ancien minuteur est annulé et un nouveau est lancé. - Cas d'usage: recherche ou autocomplétion pendant la saisie, validation d'un champ (« ce pseudo est-il libre ? »), sauvegarde automatique d'un brouillon, recalcul de la mise en page à la fin d'un redimensionnement.
-
Throttle : limiter la fréquence
- Le throttle (étranglement) exécute la fonction au plus une fois par intervalle de temps, même si l'événement continue.
- Image : un robinet réglé pour laisser passer une goutte toutes les 200 ms, quelle que soit la pression.
function throttle(fonction, intervalle = 200) { let dernierAppel = 0; return function (...args) { const maintenant = Date.now(); if (maintenant - dernierAppel >= intervalle) { dernierAppel = maintenant; fonction.apply(this, args); } }; } // ✔ La barre de progression de lecture est mise à jour au plus 5 fois par seconde const majProgression = throttle(() => { const max = document.documentElement.scrollHeight - window.innerHeight; barre.style.width = `${(window.scrollY / max) * 100}%`; }, 200); window.addEventListener("scroll", majProgression);- Cas d'usage: suivi du défilement (barre de progression, bouton « remonter »), suivi de la souris, jeux, envoi de statistiques, bouton qu'on ne doit pas pouvoir cliquer 10 fois par seconde.
- Cette version simple ignore les appels qui arrivent pendant l'intervalle : le dernier état peut donc ne pas être traité. Les versions complètes (par exemple celle de la bibliothèque Lodash) ajoutent un dernier appel en fin de rafale.
-
Debounce ou throttle ?
| Debounce | Throttle | |
|---|---|---|
| Principe | Attend la fin de la rafale | Exécute régulièrement pendant la rafale |
| Nombre d'exécutions pour une rafale de 3 secondes | 1 (à la fin) | Environ 3 s ÷ intervalle |
| Question à se poser | « Seul le résultat final m'intéresse » | « Je veux un retour visuel continu, mais pas trop fréquent » |
| Exemples | Recherche, validation, sauvegarde automatique | Défilement, souris, jeux, redimensionnement en direct |
-
Les alternatives modernes
requestAnimationFrame: pour une mise à jour visuelle (animation, position), on synchronise le travail avec l'affichage de l'écran (environ 60 fois par seconde) :
let enAttente = false; window.addEventListener("scroll", () => { if (enAttente) return; enAttente = true; requestAnimationFrame(() => { mettreAJourAffichage(); enAttente = false; }); });- Écouteurs passifs:
{ passive: true }indique au navigateur que l'écouteur n'appellera paspreventDefault(). Le défilement reste ainsi fluide :
window.addEventListener("scroll", majProgression, { passive: true });IntersectionObserver: pour savoir si un élément est visible (chargement d'images au défilement, animation à l'apparition), il remplace avantageusement l'écoute descroll.- Annuler les requêtes dépassées: avec une recherche, on peut aussi annuler la requête précédente avec
AbortControllerpour ne jamais afficher un résultat périmé.
-
Mesurer l'effet
- Comptez les appels avec
console.count()avant et après optimisation. - Dans les DevTools, l'onglet Réseau montre le nombre de requêtes envoyées et l'onglet Performance enregistre l'activité de la page pendant quelques secondes.
champRecherche.addEventListener("input", () => console.count("événement input")); const compterRecherche = debounce(() => console.count("recherche lancée"), 400); champRecherche.addEventListener("input", compterRecherche); // En tapant « bootstrap » d'un trait : input ×9, recherche lancée ×1 - Comptez les appels avec
-
Activités
Activité 1 : compter les événements
- Créez une page avec un champ de texte et une zone très haute (
height: 3000px). Comptez avecconsole.countles événementsinput,scrolletmousemovependant 5 secondes d'utilisation.
Activité 2 : recherche avec debounce
- Créez un tableau de 30 noms de composants Bootstrap. Affichez sous un champ la liste filtrée selon le texte saisi, avec un debounce de 300 ms. Affichez aussi le nombre de filtrages effectués.
- Comparez avec la version sans debounce en tapant « navbar ».
Activité 3 : barre de progression avec throttle
- Ajoutez en haut d'un long article une barre de progression Bootstrap (
progress) mise à jour au défilement avec un throttle de 100 ms et un écouteur passif.
Activité 4 : choisir la technique
- Pour chaque situation, choisissez debounce, throttle ou
IntersectionObserveret justifiez : (a) vérifier qu'un e-mail n'est pas déjà utilisé ; (b) afficher la position de la souris ; (c) charger les images quand elles approchent de l'écran ; (d) recalculer une grille de cartes quand la fenêtre est redimensionnée ; (e) empêcher un double clic sur « Payer ». - Déposez vos fichiers dans le dépôt GitHub
js-debounce-throttle.
Corrigé de l'activité 4. (a) debounce : on vérifie une fois la saisie terminée. (b) throttle (ou
requestAnimationFrame) : retour visuel régulier. (c) IntersectionObserver. (d) debounce : seul l'état final compte. (e) throttle avec un long intervalle (ou désactiver le bouton jusqu'à la réponse du serveur, ce qui est encore plus sûr).À retenir
input,scroll,resizeetmousemovese déclenchent en rafale.- Debounce: une seule exécution, après la fin de la rafale.
- Throttle: au plus une exécution par intervalle, pendant la rafale.
- Pour le visuel, pensez à
requestAnimationFrame, aux écouteurs passifs et àIntersectionObserver.
Sources : MDN Web Docs (developer.mozilla.org), OWASP (owasp.org), documentations d'ESLint, Prettier et Vitest.