Utiliser requestIdleCallback

De nombreux sites et applications ont de nombreux scripts à exécuter. Bien souvent, JavaScript doit être exécuté le plus tôt possible, sans pour autant entraver l'expérience de l'utilisateur. Si vous envoyez des données d'analyse lorsque l'utilisateur fait défiler la page ou si vous ajoutez des éléments au DOM alors qu'il appuie sur le bouton, votre application Web risque de ne plus répondre, ce qui nuit à l'expérience utilisateur.

Utilisation de requestIdleCallback pour planifier des tâches non essentielles.

La bonne nouvelle, c'est qu'il existe désormais une API qui peut vous aider: requestIdleCallback. Tout comme l'adoption de requestAnimationFrame nous a permis de planifier correctement les animations et d'optimiser nos chances d'atteindre 60 FPS, requestIdleCallback planifie des tâches quand il reste du temps libre à la fin d'une image ou lorsque l'utilisateur est inactif. Cela signifie qu'il y a une opportunité de faire votre travail sans perturber la navigation de l'utilisateur. Il est disponible à partir de Chrome 47. Vous pouvez donc l'essayer dès aujourd'hui en utilisant Chrome Canary. Il s'agit d'une fonctionnalité expérimentale. Les spécifications étant encore en cours de modification, les choses pourraient changer à l'avenir.

Pourquoi utiliser requestIdleCallback ?

Planifier vous-même les tâches non essentielles est très difficile à faire. Il est impossible de déterminer exactement le temps de rendu restant, car après l'exécution des rappels requestAnimationFrame, des calculs de style, la mise en page, la peinture et d'autres éléments internes du navigateur doivent être exécutés. Une solution de déploiement par défaut ne peut en tenir compte. Pour vous assurer qu'un utilisateur n'interagit pas d'une manière ou d'une autre, vous devez également associer des écouteurs à chaque type d'événement d'interaction (scroll, touch, click), même si vous n'en avez pas besoin pour des fonctionnalités, uniquement afin d'être absolument sûr que l'utilisateur n'interagit pas. Le navigateur, quant à lui, sait exactement combien de temps est disponible à la fin du frame et si l'utilisateur interagit. Ainsi, grâce à requestIdleCallback, nous obtenons une API qui nous permet d'utiliser tout le temps libre le plus efficacement possible.

Examinons-le plus en détail et voyons comment nous pouvons l'utiliser.

Vérification de requestIdleCallback...

requestIdleCallback n'en est qu'à ses débuts. Avant de l'utiliser, vérifiez donc qu'elle est disponible:

if ('requestIdleCallback' in window) {
    // Use requestIdleCallback to schedule work.
} else {
    // Do what you’d do today.
}

Vous pouvez également corriger son comportement, ce qui nécessite de revenir à setTimeout:

window.requestIdleCallback =
    window.requestIdleCallback ||
    function (cb) {
    var start = Date.now();
    return setTimeout(function () {
        cb({
        didTimeout: false,
        timeRemaining: function () {
            return Math.max(0, 50 - (Date.now() - start));
        }
        });
    }, 1);
    }

window.cancelIdleCallback =
    window.cancelIdleCallback ||
    function (id) {
    clearTimeout(id);
    }

L'utilisation de setTimeout n'est pas idéale, car elle ne connaît pas le temps d'inactivité comme le fait requestIdleCallback. Cependant, comme vous appeleriez votre fonction directement si requestIdleCallback n'était pas disponible, il n'y a pas pire à utiliser le shimming de cette manière. Avec le shim, si requestIdleCallback est disponible, vos appels seront redirigés silencieusement, ce qui est très pratique.

Pour le moment, supposons qu'elle existe.

Utiliser requestIdleCallback

L'appel de requestIdleCallback est très semblable à requestAnimationFrame, car il utilise une fonction de rappel comme premier paramètre:

requestIdleCallback(myNonEssentialWork);

Lorsque myNonEssentialWork est appelé, un objet deadline lui est attribué, qui contient une fonction qui renvoie un nombre indiquant le temps restant pour votre tâche:

function myNonEssentialWork (deadline) {
    while (deadline.timeRemaining() > 0)
    doWorkIfNeeded();
}

La fonction timeRemaining peut être appelée pour obtenir la dernière valeur. Lorsque timeRemaining() renvoie zéro, vous pouvez planifier un autre requestIdleCallback si vous avez encore du travail:

function myNonEssentialWork (deadline) {
    while (deadline.timeRemaining() > 0 && tasks.length > 0)
    doWorkIfNeeded();

    if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

Garantir que votre fonction est appelée

Que faites-vous en cas de forte affluence ? Vous craignez peut-être que votre rappel ne soit jamais appelé. Bien que requestIdleCallback ressemble à requestAnimationFrame, il diffère également en ce qu'il accepte un deuxième paramètre facultatif: un objet d'options avec une propriété expire (délai d'expiration). S'il est défini, ce délai donne au navigateur un délai en millisecondes pendant lequel il doit exécuter le rappel:

// Wait at most two seconds before processing events.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });

Si votre rappel est exécuté en raison du déclenchement du délai d'inactivité, vous remarquerez deux choses:

  • timeRemaining() renvoie zéro.
  • La propriété didTimeout de l'objet deadline est définie sur "true".

Si vous constatez que didTimeout est défini sur "true", vous souhaiterez probablement simplement exécuter le travail et en terminer avec:

function myNonEssentialWork (deadline) {

    // Use any remaining time, or, if timed out, just run through the tasks.
    while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
            tasks.length > 0)
    doWorkIfNeeded();

    if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

En raison de la perturbation potentielle que ce délai d'inactivité peut causer à vos utilisateurs (votre application risque de ne plus répondre ou d'être saccadée), soyez prudent lorsque vous définissez ce paramètre. Dans la mesure du possible, laissez le navigateur décider quand appeler le rappel.

Utiliser requestIdleCallback pour envoyer des données d'analyse

Voyons comment utiliser requestIdleCallback pour envoyer des données d'analyse. Dans ce cas, nous voudrions probablement suivre un événement, par exemple en appuyant sur un menu de navigation. Toutefois, comme ils s'animent habituellement à l'écran, nous voulons éviter d'envoyer immédiatement cet événement à Google Analytics. Nous allons créer un tableau des événements à envoyer et demander qu'ils soient envoyés ultérieurement:

var eventsToSend = [];

function onNavOpenClick () {

    // Animate the menu.
    menu.classList.add('open');

    // Store the event for later.
    eventsToSend.push(
    {
        category: 'button',
        action: 'click',
        label: 'nav',
        value: 'open'
    });

    schedulePendingEvents();
}

Nous devons maintenant utiliser requestIdleCallback pour traiter tous les événements en attente:

function schedulePendingEvents() {

    // Only schedule the rIC if one has not already been set.
    if (isRequestIdleCallbackScheduled)
    return;

    isRequestIdleCallbackScheduled = true;

    if ('requestIdleCallback' in window) {
    // Wait at most two seconds before processing events.
    requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
    } else {
    processPendingAnalyticsEvents();
    }
}

Comme vous pouvez le voir, j'ai défini un délai avant expiration de 2 secondes, mais cette valeur dépend de votre application. Pour les données d'analyse, il est logique qu'un délai avant expiration soit utilisé pour s'assurer que les données sont rapportées dans un délai raisonnable plutôt qu'à un moment précis dans le futur.

Enfin, nous devons écrire la fonction que requestIdleCallback exécutera.

function processPendingAnalyticsEvents (deadline) {

    // Reset the boolean so future rICs can be set.
    isRequestIdleCallbackScheduled = false;

    // If there is no deadline, just run as long as necessary.
    // This will be the case if requestIdleCallback doesn’t exist.
    if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

    // Go for as long as there is time remaining and work to do.
    while (deadline.timeRemaining() > 0 && eventsToSend.length > 0) {
    var evt = eventsToSend.pop();

    ga('send', 'event',
        evt.category,
        evt.action,
        evt.label,
        evt.value);
    }

    // Check if there are more events still to send.
    if (eventsToSend.length > 0)
    schedulePendingEvents();
}

Pour cet exemple, j'ai supposé que si requestIdleCallback n'existait pas, les données d'analyse devaient être envoyées immédiatement. Toutefois, dans une application de production, il est probablement préférable de retarder l'envoi avec un délai d'inactivité afin de s'assurer qu'il n'entre pas en conflit avec les interactions et ne provoque pas d'à-coups.

Utiliser requestIdleCallback pour effectuer des modifications DOM

requestIdleCallback peut également améliorer les performances lorsque vous devez apporter des modifications DOM non essentielles, comme l'ajout d'éléments à la fin d'une liste à chargement différé et qui ne cesse de s'allonger. Voyons comment requestIdleCallback s'intègre dans un frame classique.

Image typique.

Il est possible que le navigateur soit trop occupé pour exécuter des rappels dans un frame donné. Attendez-vous donc à ce qu'il n'y ait aucun temps libre à la fin d'un frame pour effectuer d'autres tâches. Elle est donc différente de setImmediate, qui s'exécute par frame.

Si le rappel est déclenché à la fin de l'image, il est programmé pour se déclencher après le commit du frame actuel, ce qui signifie que les modifications de style ont été appliquées et, surtout, que la mise en page a été calculée. Si nous apportons des modifications DOM dans le rappel d'inactivité, ces calculs de mise en page seront invalidés. S'il existe un type de lecture de mise en page dans le cadre suivant, par exemple getBoundingClientRect, clientWidth, etc., le navigateur doit effectuer une mise en page synchrone forcée, ce qui peut entraîner un goulot d'étranglement des performances.

Une autre raison pour laquelle les modifications DOM ne sont pas déclenchées dans le rappel d'inactivité est que l'impact de la modification du DOM sur le temps est imprévisible. Par conséquent, nous pourrions facilement dépasser le délai indiqué par le navigateur.

Il est recommandé de n'apporter des modifications DOM que dans un rappel requestAnimationFrame, car celui-ci est planifié par le navigateur en fonction de ce type de tâche. Cela signifie que notre code doit utiliser un fragment de document, qui peut ensuite être ajouté au prochain rappel requestAnimationFrame. Si vous utilisez une bibliothèque VDOM, vous devez utiliser requestIdleCallback pour apporter des modifications, mais vous devez appliquer les correctifs DOM dans le rappel requestAnimationFrame suivant, et non dans le rappel d'inactivité.

Sachant cela, examinons le code:

function processPendingElements (deadline) {

    // If there is no deadline, just run as long as necessary.
    if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

    if (!documentFragment)
    documentFragment = document.createDocumentFragment();

    // Go for as long as there is time remaining and work to do.
    while (deadline.timeRemaining() > 0 && elementsToAdd.length > 0) {

    // Create the element.
    var elToAdd = elementsToAdd.pop();
    var el = document.createElement(elToAdd.tag);
    el.textContent = elToAdd.content;

    // Add it to the fragment.
    documentFragment.appendChild(el);

    // Don't append to the document immediately, wait for the next
    // requestAnimationFrame callback.
    scheduleVisualUpdateIfNeeded();
    }

    // Check if there are more events still to send.
    if (elementsToAdd.length > 0)
    scheduleElementCreation();
}

Ici, je crée l'élément et j'utilise la propriété textContent pour le remplir, mais votre code de création de l'élément est probablement plus complexe. Une fois l'élément créé, scheduleVisualUpdateIfNeeded est appelé, ce qui configure un seul rappel requestAnimationFrame qui, à son tour, ajoute le fragment de document au corps:

function scheduleVisualUpdateIfNeeded() {

    if (isVisualUpdateScheduled)
    return;

    isVisualUpdateScheduled = true;

    requestAnimationFrame(appendDocumentFragment);
}

function appendDocumentFragment() {
    // Append the fragment and reset.
    document.body.appendChild(documentFragment);
    documentFragment = null;
}

Tout va bien. Nous constaterons désormais beaucoup moins d'à-coups lorsque vous ajoutez des éléments au DOM. Parfait !

Questions fréquentes

  • Y a-t-il un polyfill ? Malheureusement non, mais un correctif permet d'obtenir une redirection transparente vers setTimeout. Cette API existe, car elle comble une lacune bien réelle dans la plate-forme Web. Il est difficile de déduire un manque d'activité, mais il n'existe aucune API JavaScript pour déterminer le temps libre à la fin du frame. Dans le meilleur des cas, vous devez donc faire des suppositions. Les API telles que setTimeout, setInterval ou setImmediate peuvent être utilisées pour planifier des tâches, mais elles ne sont pas programmées pour éviter les interactions utilisateur comme requestIdleCallback.
  • Que se passe-t-il si je dépasse le délai ? Si timeRemaining() renvoie zéro, mais que vous choisissez d'exécuter votre application plus longtemps, vous pouvez le faire sans craindre que le navigateur n'interrompe votre travail. Cependant, le navigateur vous donne le délai nécessaire pour essayer de garantir une expérience fluide à vos utilisateurs. Par conséquent, à moins qu'il n'y ait une très bonne raison, vous devez toujours respecter la date limite.
  • Existe-t-il une valeur maximale que timeRemaining() renverra ? Oui, elle est actuellement de 50 ms. Lorsque vous tentez de gérer une application responsive, toutes les réponses aux interactions utilisateur doivent rester inférieures à 100 ms. Dans la plupart des cas, si l'utilisateur interagit, la fenêtre de 50 ms doit permettre au rappel d'inactivité de se terminer et de permettre au navigateur de répondre aux interactions de l'utilisateur. Vous pouvez recevoir plusieurs rappels d'inactivité programmés dos à dos (si le navigateur détermine qu'il y a suffisamment de temps pour les exécuter).
  • Y a-t-il des tâches que je ne devrais pas effectuer dans un requestIdleCallback ? Idéalement, le travail que vous effectuez doit être divisé en petits morceaux (microtâches) ayant des caractéristiques relativement prévisibles. Par exemple, la modification du DOM en particulier aura des temps d'exécution imprévisibles, car elle déclenchera les calculs de style, la mise en page, le dessin et la composition. Par conséquent, vous ne devez apporter des modifications DOM que dans un rappel requestAnimationFrame, comme suggéré ci-dessus. Il faut également se méfier de résoudre (ou de rejeter) des promesses, car les rappels s'exécutent immédiatement après la fin du rappel inactif, même s'il ne reste plus de temps.
  • Vais-je toujours obtenir une requestIdleCallback à la fin d'un frame ? Non, pas toujours. Le navigateur planifiera le rappel chaque fois qu'il y aura du temps libre à la fin d'un frame ou pendant les périodes où l'utilisateur est inactif. Vous ne devez pas vous attendre à ce que le rappel soit appelé par frame, et si vous avez besoin qu'il s'exécute dans un délai donné, vous devez utiliser le délai avant expiration.
  • Puis-je avoir plusieurs rappels requestIdleCallback ? Oui, tout comme vous pouvez avoir plusieurs rappels requestAnimationFrame. Cependant, n'oubliez pas que si votre premier rappel utilise le temps restant pendant son rappel, il ne restera plus de temps pour les autres rappels. Les autres rappels devront alors attendre que le navigateur soit à nouveau inactif avant de pouvoir être exécutés. Selon le travail que vous essayez d'effectuer, il peut être préférable de disposer d'un seul rappel d'inactivité et de répartir le travail à ce niveau. Vous pouvez également utiliser le délai avant expiration pour vous assurer qu'aucun rappel ne manque de temps.
  • Que se passe-t-il si je définis un nouveau rappel d'inactivité au sein d'un autre rappel ? Le nouveau rappel d'inactivité sera programmé pour s'exécuter dès que possible, à partir du frame suivant (et non de l'image actuelle).

Inactif, non !

requestIdleCallback est un excellent moyen de vous assurer que vous pouvez exécuter votre code sans gêner l'utilisateur. Il est simple à utiliser et très flexible. Cependant, nous n'en sommes qu'aux prémices et les spécifications ne sont pas totalement réglées. Vos commentaires sont donc les bienvenus.

Découvrez-le dans Chrome Canary, essayez vos projets et donnez-nous votre avis !