W3docs

JavaScript Debounce e Throttle

Scopri come limitare la frequenza delle funzioni in JavaScript con debounce e throttle: cosa fanno, come implementarle e quando usarle.

Alcuni eventi si attivano molto più spesso di quanto si possa rispondere utilmente. Digitare in una casella di ricerca attiva un evento input ad ogni tasto premuto; scorrere una pagina può emettere centinaia di eventi scroll al secondo; resize e mousemove sono altrettanto prolissi. Se ogni evento esegue operazioni costose — una richiesta di rete, un calcolo del layout, un re-render — l'applicazione si inceppa. Debounce e throttle sono due piccoli wrapper che limitano la frequenza con cui una funzione viene eseguita, mantenendo alta la reattività senza cambiare ciò che la funzione fa.

Entrambi sono classici pattern decorator: prendono una funzione e restituiscono una nuova funzione con lo stesso comportamento più una regola di limitazione della frequenza. Sono costruiti sulle closure per ricordare lo stato tra le chiamate e sui timer come setTimeout per rinviare o controllare l'esecuzione.

Il Concetto Fondamentale

Le due tecniche rispondono alla stessa domanda — "con quale frequenza dovrebbe eseguire?" — in modi opposti:

  • Debounce attende una pausa. Rimanda la chiamata finché non sono trascorsi N millisecondi dall'ultima invocazione. Se le chiamate continuano ad arrivare, il timer si azzera continuamente e la funzione non viene mai eseguita. Pensate: "aspetta il silenzio."
  • Throttle impone una cadenza regolare. Lascia che la funzione venga eseguita al massimo una volta ogni N millisecondi, indipendentemente da quante volte viene chiamata nel frattempo. Pensate: "battito regolare."
AspettoDebounceThrottle
Si attiva quandoL'attività si interrompe per N msAl massimo una volta ogni N ms
Durante una rafficaNiente viene eseguito fino alla fine della rafficaViene eseguito a intervalli fissi
Modello mentale"Aspetta il silenzio""Cadenza regolare"
Adatto perRicerca durante la digitazione, salvataggio automatico, ridimensionamento completatoTracciamento dello scroll, drag, scroll infinito

Debounce

Una funzione con debounce azzera qualsiasi timer in sospeso ad ogni chiamata e ne pianifica uno nuovo. Solo quando le chiamate si fermano per delay millisecondi la funzione sottostante viene effettivamente eseguita.

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

Due dettagli rendono questa implementazione robusta. Il wrapper raccoglie tutti gli argomenti con i parametri rest (...args) e li inoltra, in modo che la funzione sottostante riceva esattamente ciò che il chiamante ha passato. E invoca fn con fn.apply(this, args) in modo che il this originale venga preservato — importante quando la funzione con debounce è un metodo su un object. (Vedi call e apply e function binding per capire perché inoltrare this è importante.)

Eccolo in azione. Chiamare ripetutamente la funzione wrapper attiva solo un'esecuzione reale, dopo che l'attività si stabilizza:

javascript— editable

Poiché ogni tasto premuto azzera il clock, debounce è ideale ogni volta che si vuole reagire dopo che l'utente ha finito: ricerca durante la digitazione, salvataggio automatico di una bozza, validazione di un campo una volta terminata la digitazione, o ricalcolo di un layout solo quando il ridimensionamento della finestra si è stabilizzato.

Throttle

Una funzione con throttle viene eseguita immediatamente, poi ignora ulteriori chiamate fino allo scadere di un periodo di raffreddamento. Questo garantisce una frequenza massima anziché attendere una pausa.

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

Il flag inThrottle, mantenuto nella closure, funge da cancello. La prima chiamata passa e il cancello si chiude; le chiamate durante il periodo di raffreddamento vengono scartate; quando il timer scatta, il cancello si riapre per la chiamata successiva.

javascript— editable

Throttle si adatta a qualsiasi cosa che scorra continuamente e dove si desiderino aggiornamenti regolari anziché ogni singolo evento: tracciamento della posizione di scroll, gestione di mousemove durante un drag, caricamento di altro contenuto nello scroll infinito, o limitazione della frequenza con cui si interroga un'API con rate limiting.

Edge Iniziale vs. Edge Finale

In entrambi i wrapper c'è una scelta di progettazione sottile: la funzione dovrebbe attivarsi sull'edge iniziale (la prima chiamata, immediatamente) o sull'edge finale (dopo il ritardo/raffreddamento)?

  • Il debounce sopra è trailing: non accade nulla finché l'attività non si ferma. Un debounce sull'edge iniziale verrebbe eseguito alla prima chiamata e poi ignorerebbe le successive.
  • Il throttle sopra è leading: si attiva immediatamente e poi controlla l'accesso. Un throttle sull'edge finale verrebbe eseguito anche una volta alla fine della finestra temporale per catturare il valore finale.

Questi comportamenti degli edge hanno importanza pratica — un throttle trailing sullo scroll, ad esempio, assicura di non perdere la posizione finale dello scroll quando l'utente si ferma.

Informazione

Per il codice in produzione, preferite un'implementazione collaudata come _.debounce e _.throttle di lodash. Gestiscono gli edge iniziali e finali, un'API cancel()/flush(), e un'opzione maxWait (in modo che una funzione con debounce venga comunque eseguita durante un'attività continua). Capire le versioni di base sopra è essenziale, ma raramente è necessario svilupparne una propria.

Un Esempio Reale con il DOM

Collegare debounce a un input di ricerca è il caso d'uso canonico. Si collega un solo listener (vedi gestione degli eventi nel DOM) e si lascia che il wrapper decida quando il lavoro viene effettivamente eseguito:

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);

Ora la richiesta di rete si attiva solo quando l'utente si ferma per 400 ms, invece che ad ogni tasto — una casella di ricerca che prima inviava una dozzina di richieste per hello ora ne invia una sola. Si noti che il listener riceve l'object event del DOM e, poiché il nostro wrapper inoltra ogni argomento, search lo riceve intatto.

Attenzione

I timer e i listener mantengono riferimenti, quindi è necessario rimuoverli quando non sono più necessari. In una single-page app o in un componente, rimuovere il listener al teardown (ad esempio, all'unmount del componente) e azzerare qualsiasi timer in sospeso per evitare memory leak e callback che si attivano su elementi che non esistono più:

input.removeEventListener('input', debouncedSearch);

Un debounce in produzione tipicamente espone anche un metodo cancel() che chiama clearTimeout al posto vostro.

Scegliere tra i Due

Quando non si è sicuri di quale usare, chiedetevi cosa vi interessa:

  • Vi interessa solo lo stato finale dopo una raffica di attività (il termine di ricerca completato, la dimensione della finestra stabilizzata)? Usate debounce.
  • Volete un feedback continuo e regolare durante l'attività (progresso dello scroll, posizione di un elemento trascinato)? Usate throttle.

Entrambi sono leggeri, indipendenti dal framework, e si basano direttamente su closure e timer — le stesse fondamenta alla base delle arrow function che catturano this e degli strumenti di scheduling già visti.

Metti alla Prova le Tue Conoscenze

Pratica
Quale tecnica esegue la funzione al massimo una volta per intervallo di tempo fisso, indipendentemente da quante volte viene chiamata?
Quale tecnica esegue la funzione al massimo una volta per intervallo di tempo fisso, indipendentemente da quante volte viene chiamata?
Pratica
Vuoi inviare una richiesta di ricerca solo dopo che l'utente ha smesso di digitare. Qual è lo strumento giusto?
Vuoi inviare una richiesta di ricerca solo dopo che l'utente ha smesso di digitare. Qual è lo strumento giusto?
Pratica
Perché i wrapper di esempio debounce e throttle chiamano la funzione originale con `fn.apply(this, args)` invece di semplicemente `fn(args)`?
Perché i wrapper di esempio debounce e throttle chiamano la funzione originale con `fn.apply(this, args)` invece di semplicemente `fn(args)`?
Was this page helpful?