Utiliser requestIdleCallback

De nombreux sites et applications ont de nombreux scripts à exécuter. Bien souvent, vous devez exécuter votre code JavaScript dès que possible, mais vous ne voulez pas qu'il gêne 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 et de nuire à l'expérience utilisateur.

Utiliser requestIdleCallback pour planifier les tâches non essentielles.

Heureusement, il existe désormais une API qui peut vous aider: requestIdleCallback. De la même manière que l'adoption de requestAnimationFrame nous a permis de programmer correctement des animations et d'optimiser nos chances d'atteindre 60 FPS, requestIdleCallback planifie le travail lorsqu'il reste du temps à la fin d'un frame ou lorsque l'utilisateur est inactif. Cela signifie qu’il y a une possibilité de faire votre travail sans interférer avec l’utilisateur. Il est disponible à partir de Chrome 47, vous pouvez donc essayer dès aujourd'hui en utilisant Chrome Canary ! Il s'agit d'une caractéristique expérimentale dont les spécifications sont encore en cours de modification. Les choses pourraient donc changer à l'avenir.

Pourquoi utiliser requestIdleCallback ?

Il est très difficile de planifier vous-même les tâches non essentielles. Il est impossible de déterminer exactement combien de temps il reste, car après l'exécution des rappels requestAnimationFrame, des calculs de style, une mise en page, un effet de peinture et d'autres éléments internes du navigateur doivent être exécutés. Une solution personnalisée ne peut pas tenir compte de ces éléments. Pour vous assurer qu'un utilisateur n'interagit pas d'une manière ou d'une autre, vous devez également joindre des écouteurs à chaque type d'événement d'interaction (scroll, touch, click), même si vous n'en avez pas besoin pour la fonctionnalité, juste pour être absolument sûr que l'utilisateur n'interagit pas. Le navigateur, en revanche, 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 notre temps libre de la manière la plus efficace possible.

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

Recherche de requestIdleCallback

requestIdleCallback n'en est qu'à ses débuts. Avant de l'utiliser, vérifiez qu'il peut être utilisé:

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 n'est pas informée du temps d'inactivité, contrairement à requestIdleCallback. Toutefois, comme vous appeleriez votre fonction directement si requestIdleCallback n'était pas disponible, vous n'avez rien à craindre de cette façon. Avec le shim, si requestIdleCallback est disponible, vos appels sont redirigés silencieusement, ce qui est très bien.

Pour l'instant, supposons toutefois qu'elle existe.

Utiliser requestIdleCallback

L'appel de requestIdleCallback est très semblable à requestAnimationFrame, dans la mesure où il utilise une fonction de rappel comme premier paramètre:

requestIdleCallback(myNonEssentialWork);

Lorsque myNonEssentialWork est appelé, il reçoit un objet deadline contenant 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 à effectuer:

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

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

La garantie de la fonction s'appelle

Que faites-vous si vous êtes vraiment très occupé ? Vous craignez peut-être que votre rappel ne soit jamais appelé. Bien que requestIdleCallback ressemble à requestAnimationFrame, la différence est également qu'il accepte un deuxième paramètre facultatif: un objet d'options avec une propriété Timeout. S'il est défini, ce délai indique au navigateur le 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 avant expiration, vous remarquerez deux choses:

  • timeRemaining() renvoie zéro.
  • La propriété didTimeout de l'objet deadline aura la valeur "true".

Si vous voyez que didTimeout est vrai, vous souhaiterez probablement simplement exécuter la tâche et ne plus l'utiliser:

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 l'interruption potentielle de ce délai pour vos utilisateurs (en raison de ce travail, votre application risque de ne plus répondre ou d'être saccadée), définissez ce paramètre avec prudence. Lorsque cela est 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 comme, par exemple, appuyer sur un menu de navigation. Toutefois, comme ils s'animent normalement à l'écran, nous évitons d'envoyer immédiatement cet événement à Google Analytics. Nous allons créer un tableau d'événements à envoyer et demander à les envoyer 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 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();
    }
}

Vous pouvez voir ici que j'ai défini un délai avant expiration de deux secondes, mais cette valeur dépend de votre application. Pour les données d'analyse, il est logique d'utiliser un délai avant expiration pour s'assurer que les données sont rapportées dans un délai raisonnable plutôt qu'à un moment ultérieur.

Pour finir, 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 préférable de retarder l'envoi avec un délai avant expiration 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 considérablement les performances lorsque vous devez apporter des modifications DOM non essentielles, comme ajouter des éléments à la fin d'une liste qui ne cesse de s'allonger et au chargement différé. Voyons comment requestIdleCallback s'intègre dans un cadre typique.

Une image standard.

Il est possible que le navigateur soit trop occupé pour exécuter des rappels dans un frame donné. Ne vous attendez donc pas à ce qu'il y ait peu de 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 du frame, il sera programmé pour se déclencher après le commit du frame actuel. Cela 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 inactif, ces calculs de mise en page ne seront plus valides. S'il existe un type de lecture de mise en page dans le frame suivant (par exemple, getBoundingClientRect, clientWidth, etc.), le navigateur devra effectuer une mise en page synchrone forcée, ce qui peut constituer un goulot d'étranglement des performances.

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

La bonne pratique consiste à n'apporter des modifications DOM que dans un rappel requestAnimationFrame, car celui-ci est planifié par le navigateur en pensant à ce type de tâche. Cela signifie que notre code devra utiliser un fragment de document, qui pourra ensuite être ajouté dans le prochain rappel requestAnimationFrame. Si vous utilisez une bibliothèque VDOM, vous utiliserez requestIdleCallback pour apporter des modifications, mais vous appliquerez les correctifs DOM dans le prochain rappel requestAnimationFrame, et non le rappel inactif.

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 renseigner, mais il est probable que le code de création de votre élément soit plus complexe. Une fois l'élément créé, scheduleVisualUpdateIfNeeded est appelé, lequel définit un seul rappel requestAnimationFrame qui, à son tour, ajoutera 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. Les à-coups sont désormais beaucoup moins fréquents lors de l'ajout d'éléments au DOM. Parfait !

Questions fréquentes

  • Y a-t-il un polyfill ? Malheureusement non, mais un shim est disponible si vous souhaitez effectuer une redirection transparente vers setTimeout. Cette API s'explique par le fait qu'elle comble une lacune très concrète avec la plate-forme Web. L'inférence d'un manque d'activité est difficile, mais il n'existe aucune API JavaScript permettant de déterminer le temps libre à la fin du frame, vous devez donc au mieux 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 une interaction utilisateur comme c'est le cas pour 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 plus longtemps, vous pouvez le faire sans craindre que le navigateur n'interrompe votre travail. Cependant, le navigateur vous donne un délai pour essayer de garantir une expérience fluide à vos utilisateurs. Par conséquent, sauf s'il existe une très bonne raison, vous devez toujours respecter la date limite.
  • Y a-t-il une valeur maximale que timeRemaining() renvoie ? Oui, il est actuellement à 50 ms. Lorsque vous essayez de maintenir une application responsive, toutes les réponses aux interactions des utilisateurs doivent rester inférieures à 100 ms. Dans la plupart des cas, si l'utilisateur interagit au cours de cette fenêtre de 50 ms, le rappel d'inactivité doit être terminé et le navigateur doit répondre à ces interactions. Vous pouvez recevoir plusieurs rappels d'inactivité programmés à la suite (si le navigateur détermine qu'il y a suffisamment de temps pour les exécuter).
  • Y a-t-il quelque chose que je ne devrais pas faire dans un requestIdleCallback ? Idéalement, le travail que vous effectuez doit se faire 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 des calculs de style, la mise en page, la peinture et la composition. Par conséquent, vous ne devez apporter des modifications DOM à un rappel requestAnimationFrame que, comme suggéré ci-dessus. Vous devez également vous méfier de la résolution (ou du rejet) 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 programme le rappel chaque fois qu'il reste du temps à 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. Si vous souhaitez qu'il s'exécute dans un délai donné, utilisez le délai avant expiration.
  • Puis-je avoir plusieurs rappels requestIdleCallback ? Oui, dans la mesure où vous pouvez avoir plusieurs rappels requestAnimationFrame. N'oubliez pas que si votre premier rappel utilise le temps restant, il ne vous restera plus de temps pour les autres rappels. Les autres rappels devront alors attendre que le navigateur soit à nouveau inactif avant de pouvoir s'exécuter. En fonction du travail que vous essayez d'effectuer, il peut être préférable de n'avoir qu'un seul rappel en cas d'inactivité et de répartir le travail dans celui-ci. 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 inactif dans un autre ? Le nouveau rappel inactif sera programmé pour s'exécuter dès que possible, à partir du frame suivant (plutôt que du frame actuel).

Inactif !

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, ce n'est qu'un début, et les spécifications n'ont pas encore été finalisées. Tous vos commentaires sont donc les bienvenus.

Découvrez-la dans Chrome Canary, testez-la sur vos projets et donnez-nous votre avis !