Kévin Subileau

Espace personnel

clock

Debounce et throttle : limiter les appels successifs à une fonction Javascript

Lorsque l'on développe une application Web réagissant à certains évènements en Javascript, comme le redimensionnement de la fenêtre, le mouvement de la souris ou la frappe au clavier, les fonctions rattachées à ces événements risquent d'être appelées très fréquemment. Si le code contenu dans ces fonctions est coûteux en ressources, cela peut ralentir considérablement l'application Web.

Pour améliorer les performances, ou plutôt ne pas les plomber, il existe deux techniques simples, appelées debounce et throttle. Toutes deux permettent de réduire le nombre d'appels à une fonction dans un intervalle de temps donné. En effet, il est souvent inutile d'exécuter ces fonctions à chaque fois que l’événement est déclenché.

Imaginons par exemple que l'on souhaite synchroniser sur le serveur, via des appels AJAX, le contenu d'un champ de saisie au fur et à mesure que l'utilisateur tape le texte (comme le fait Google Docs par exemple). Dans ce cas, plutôt que de solliciter le serveur à chaque caractère saisi ou inutilement lorsque le texte reste inchangé, il peut être plus judicieux de sauvegarder le texte toutes les 5 secondes tant que l'utilisateur le modifie, et de ne plus faire de requêtes lorsque que le texte reste inchangé. Et c'est exactement ce que la fonction throttle permet de faire.

Cependant, bien que ces deux techniques soit assez similaires, il existe une différence fondamentale dans leurs fonctionnements qu'il est important de bien comprendre pour les utiliser correctement.

Je vais donc vous présenter ces deux méthodes l'une après l'autre, en vous expliquant leurs fonctionnements et en vous donnant le code Javascript nécessaire pour les mettre en place.

Debounce

Commençons par la fonction debounce. Cette technique a pour but de n'appeler qu'une seule fois une méthode au début ou à la fin d'une succession de déclenchements d'un événement. Voici tout d'abord le code source de la fonction en question :

/**
 * Retourne une fonction qui, tant qu'elle continue à être invoquée,
 * ne sera pas exécutée. La fonction ne sera exécutée que lorsque
 * l'on cessera de l'appeler pendant plus de N millisecondes.
 * Si le paramètre `immediate` vaut vrai, alors la fonction 
 * sera exécutée au premier appel au lieu du dernier.
 * Paramètres :
 *  - func : la fonction à `debouncer`
 *  - wait : le nombre de millisecondes (N) à attendre avant 
 *           d'appeler func()
 *  - immediate (optionnel) : Appeler func() à la première invocation
 *                            au lieu de la dernière (Faux par défaut)
 *  - context (optionnel) : le contexte dans lequel appeler func()
 *                          (this par défaut)
 */
function debounce(func, wait, immediate, context) {
    var result;
    var timeout = null;
    return function() {
        var ctx = context || this, args = arguments;
        var later = function() {
            timeout = null;
            if (!immediate) result = func.apply(ctx, args);
        };
        var callNow = immediate && !timeout;
        // Tant que la fonction est appelée, on reset le timeout.
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) result = func.apply(ctx, args);
        return result;
    };
}

Pour mieux comprendre son fonctionnement, regardons le schéma ci dessous :

js-debounce

Chronogramme de la fonction debounce

Sur la première ligne de cette figure, chaque rectangle bleu représente un déclenchement de l’événement écouté (keydown par exemple). Lorsque deux déclenchements sont espacés par une durée inférieure à une valeur prédéfinie (paramètre wait), on va dire qu'ils appartiennent à une même séquence. Si la durée est supérieure, alors cela marque le début d'une nouvelle séquence.

Sur la deuxième ligne, les rectangles verts représentent les exécutions de la fonction "débouncé" (paramètre func). On remarque alors que la fonction debounce permet d'attendre la fin d'une séquence d'évènement pour effectuer le traitement associé. Si maintenant on reprend l'exemple de la synchronisation d'un champ de texte, cela signifie que l'appel AJAX sera effectué uniquement lorsque l'utilisateur cessera de modifier le contenu pendant une durée supérieure à wait.

Par ailleurs, dans certains cas, il peut être plus intéressant d'exécuter le traitement au début de la séquence plutôt qu'à la fin (comme le représente les rectangles violets). Cela est possible simplement en définissant le paramètre optionnel immediate à true.

L'utilisation concrète de cette fonction est assez simple. Voici un exemple :

function traiterEvenement() {
	// Fonction de traitement de l'évenement
        // (sauvegarde par appel AJAX par exemple)
}

// On limite l’exécution de la fonction traiterEvenement à une fois
// toutes les 5000 millisecondes (5s) au maximum.
var traiterEvenementDebounce = debounce(traiterEvenement, 5000);

// On s'enregistre sur l’événement de saisie clavier
document.querySelectorAll('#field')
        .addEventListener('keydown', traiterEvenementDebounce);

Cependant, ce mode de fonctionnement peut parfois poser problème. Ici par exemple, si l'utilisateur tape son texte en continu pendant plusieurs minutes, il ne sera pas sauvegardé sur le serveur puisque la séquence ne se termine pas. Selon les cas, cela peut être souhaitable ou gênant, c'est à vous de voir. Pour éviter cela, il y a deux solutions :

  • Soit réduire l'intervalle entre les séquences (de 5s à 500ms par exemple), en se disant que l'utilisateur ne pourra pas taper longtemps sur son clavier sans jamais s'arrêter moins de 500ms. C'est plutôt vrai, mais du coup on augmente potentiellement le nombre d’exécutions, donc ça risque d'être plus lent et ça perd un peu de son intérêt...
  • Soit utiliser la fonction throttle, qui propose un fonctionnement un peu différent.

Throttle

Parlons donc maintenant de la fonction throttle. Ici, comme le montre le chronogramme ci-dessous, on va exécuter la fonction de traitement de l'événement de manière périodique durant la séquence de déclenchements.

js-throttle

Chronogramme de la fonction throttle

Sur cette figure, les rectangles de couleur de la deuxième ligne représentes les exécutions de la fonction func. Attention, par défaut, seuls les rectangles violets sont effectivement exécutés. Pour inclure également les exécutions en début et/ou en fin de séquence (rectangles jaunes et verts), il faut définir respectivement les paramètres leading et/ou trailing à true.

Si on applique cette fonction à notre exemple, cela signifie que le texte du champ de saisie sera sauvegardé sur le serveur par un appel AJAX toutes les 5 secondes par exemple (paramètre wait), et ce tant que l'utilisateur change le contenu. Et si le texte n'est pas modifié, aucun appel n'est effectué. Ce comportement est donc idéal dans ce cas, puisqu'on limite le nombre d'appels au strict nécessaire pour ne pas risquer une perte de données.

Voici donc maintenant le code de la fonction throttle :

/**
 * Retourne une fonction qui, tant qu'elle est appelée,
 * n'est exécutée au plus qu'une fois toutes les N millisecondes.
 * Paramètres :
 *  - func : la fonction à contrôler
 *  - wait : le nombre de millisecondes (période N) à attendre avant 
 *           de pouvoir exécuter à nouveau la function func()
 *  - leading (optionnel) : Appeler également func() à la première
 *                          invocation (Faux par défaut)
 *  - trailing (optionnel) : Appeler également func() à la dernière
 *                           invocation (Faux par défaut)
 *  - context (optionnel) : le contexte dans lequel appeler func()
 *                          (this par défaut)
 */
function throttle(func, wait, leading, trailing, context) {
    var ctx, args, result;
    var timeout = null;
    var previous = 0;
    var later = function() {
        previous = new Date;
        timeout = null;
        result = func.apply(ctx, args);
    };
    return function() {
        var now = new Date;
        if (!previous && !leading) previous = now;
        var remaining = wait - (now - previous);
        ctx = context || this;
        args = arguments;
        // Si la période d'attente est écoulée
        if (remaining <= 0) {
            // Réinitialiser les compteurs
            clearTimeout(timeout);
            timeout = null;
            // Enregistrer le moment du dernier appel
            previous = now;
            // Appeler la fonction
            result = func.apply(ctx, args);
        } else if (!timeout && trailing) {
            // Sinon on s’endort pendant le temps restant
            timeout = setTimeout(later, remaining);
        }
        return result;
    };
};

L'utilisation de cette fonction est semblable à la fonction debounce, hormis les arguments optionnels qui sont différents. Voici un exemple :

function traiterEvenement() {
	// Fonction de traitement de l'évenement
        // (sauvegarde par appel AJAX par exemple)
}

// Ici il est utile d'activer le `trailing` pour traiter le dernier événement
var traiterEvenementThrottle = throttle(traiterEvenement, 5000, false, true);

// On s'enregistre sur l’événement de saisie clavier
document.querySelectorAll('#field')
        .addEventListener('keydown', traiterEvenementThrottle);

Les applications possibles de ces fonctions sont très nombreuses. Ici, j'ai pris l'exemple d'une fonctionnalité d'enregistrement automatique. Mais on peut aussi les utiliser pour repositionner des éléments lors du redimensionnement de la fenêtre, enregistrer des événements sur Piwik sans trop de répétitions, ...

Notez que certaines bibliothèques Javascript, comme Lodash ou UnderscoreJS, fournissent directement ces fonctions. Les implémentations que je vous propose ci-dessus en sont d'ailleurs inspirées. Inutile donc de les dupliquer si vous utilisez déjà une de ces bibliothèques pour votre application Web.

Enfin, bien que je parle ici de Web et de Javascript, je pense qu'il est tout à fait envisageable de reprendre le principe pour d'autres types de développement, y compris des applications bureaux en C++, C# ou Java par exemple. Étant donné la singularité de la syntaxe Javascript, il faudra en revanche revoir entièrement l'implémentation...

Voilà, j'espère que cet article vous aura aidé à comprendre l'utilité et le fonctionnement de ces fonctions. Si vous avez des questions, n'hésitez pas à me les poser en commentaire ;) .

Maj 12/08/2015 :  En complément de cet article, je vous conseille de voir également ce tutoriel vidéo que l'ami Grafikart a publié ce jour.

Commentaire

Laisser un commentaire

ou

Champs Requis *.

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>