Utiliser requestIdleCallback

De nombreux sites et applications ont de nombreux scripts à exécuter. Votre code JavaScript doit souvent être exécuté le plus tôt possible, sans pour autant entraver l'expérience de l'utilisateur. Si vous envoyez des données analytiques 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 peut ne plus répondre, ce qui entraîne une mauvaise 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. De la même manière que l'adoption de requestAnimationFrame nous a permis de planifier correctement les animations et de maximiser nos chances d'atteindre 60 FPS, requestIdleCallback planifie le travail lorsqu'il y a du temps libre à la fin d'un frame ou lorsque l'utilisateur est inactif. Vous avez donc la possibilité de faire votre travail sans gêner 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, et les spécifications sont encore en cours d'évolution. Les choses peuvent donc changer à l'avenir.

Pourquoi utiliser requestIdleCallback ?

Planifier vous-même des tâches non essentielles est très difficile. Il est impossible de déterminer exactement combien de temps de frame reste, car après l'exécution des rappels requestAnimationFrame, des calculs de style, de mise en page, de peinture et d'autres éléments internes du navigateur doivent s'exécuter. Une solution maison ne peut pas tenir compte de tout cela. 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, en revanche, sait exactement combien de temps est disponible à la fin du frame et si l'utilisateur interagit. Par conséquent, grâce à requestIdleCallback, nous obtenons une API qui nous permet d'utiliser tout le temps libre le plus efficacement possible.

Examinons-le de plus près pour voir comment nous pouvons l'utiliser.

Recherche de requestIdleCallback

requestIdleCallback est encore en phase de développement. Avant de l'utiliser, vérifiez qu'il 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 requestIdleCallback, mais comme vous appelleriez directement votre fonction si requestIdleCallback n'était pas disponible, vous ne perdez rien à utiliser un shim de cette manière. Avec le shim, si requestIdleCallback est disponible, vos appels sont redirigés de manière silencieuse, ce qui est excellent.

Pour l'instant, supposons qu'il existe.

Utiliser requestIdleCallback

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

requestIdleCallback(myNonEssentialWork);

Lorsque myNonEssentialWork est appelé, il reçoit un objet deadline qui contient une fonction qui renvoie un nombre indiquant le temps restant pour votre travail:

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 l'appel de votre fonction

Que faites-vous si vous êtes très occupé ? 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). Si ce délai avant expiration est défini, il indique au navigateur le délai (en millisecondes) au bout duquel 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élai avant expiration, vous remarquerez deux choses:

  • timeRemaining() renvoie zéro.
  • La propriété didTimeout de l'objet deadline sera 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 des perturbations potentielles que ce délai d'inactivité peut entraîner pour vos utilisateurs (votre application peut devenir non réactive ou 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 normalement à l'écran, nous souhaitons éviter d'envoyer cet événement immédiatement à Google Analytics. Nous allons créer un tableau d'événements à envoyer et demander qu'ils soient envoyés à un moment donné:

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 allons 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 analytiques, il est logique d'utiliser un délai avant expiration afin de s'assurer que les données sont enregistrées dans un délai raisonnable plutôt que simplement à un moment donné.

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 analytiques 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, telles que l'ajout d'éléments à la fin d'une liste en croissance constante chargée de manière différée. Voyons comment requestIdleCallback s'intègre dans un frame classique.

Cadre type.

Il est possible que le navigateur soit trop occupé pour exécuter des rappels dans un frame donné. Vous ne devez donc pas vous attendre à ce qu'il y ait du temps libre à la fin d'un frame pour effectuer d'autres tâches. C'est ce qui le distingue d'éléments comme setImmediate, qui s'exécutent 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 au DOM dans le rappel d'inactivité, ces calculs de mise en page seront invalidés. Si des lectures de mise en page sont effectuées dans le frame suivant (par exemple, getBoundingClientRect, clientWidth, etc.), le navigateur doit 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 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 ne modifier le DOM que dans un rappel requestAnimationFrame, car il est planifié par le navigateur en tenant compte de ce type de travail. 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 devez utiliser requestIdleCallback pour apporter des modifications, mais 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 utilise la propriété textContent pour le renseigner, mais il est probable que votre code de création d'éléments soit plus complexe. Après avoir créé l'élément, 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;
}

Si tout va bien, nous devrions constater une réduction importante des à-coups lors de l'ajout d'éléments au DOM. Parfait !

Questions fréquentes

  • Existe-t-il un polyfill ? Malheureusement non, mais un correctif permet d'obtenir une redirection transparente vers setTimeout. Cette API existe parce qu'elle comble un vide très réel dans la plate-forme Web. Il est difficile d'inférer un manque d'activité, mais aucune API JavaScript n'existe pour déterminer la quantité de temps libre à la fin du frame. Vous devez donc faire des suppositions au mieux. 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 l'opération plus longtemps, vous pouvez le faire sans craindre que le navigateur 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, il est actuellement de 50 ms. Pour maintenir une application réactive, toutes les réponses aux interactions utilisateur doivent être inférieures à 100 ms. Si l'utilisateur interagit, la fenêtre de 50 ms devrait, dans la plupart des cas, permettre l'exécution du rappel d'inactivité et la réponse du navigateur 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).
  • Existe-t-il un type de travail que je ne dois pas effectuer dans un requestIdleCallback ? Idéalement, votre travail doit être divisé en petites tâches (microtâches) dont les caractéristiques sont 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, une mise en page, une peinture et un compositing. Par conséquent, vous ne devez apporter des modifications au DOM que dans un rappel requestAnimationFrame, comme suggéré ci-dessus. Veillez également à ne pas résoudre (ou refuser) de promesses, car les rappels s'exécutent immédiatement après la fin du rappel d'inactivité, 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 planifie le rappel chaque fois qu'il y a 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. Si vous devez l'exécuter 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. N'oubliez pas, cependant, 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 ensuite attendre que le navigateur soit à nouveau inactif avant de pouvoir être exécutés. Selon la tâche que vous essayez d'effectuer, il peut être préférable d'avoir un seul rappel d'inactivité et de diviser la tâche en conséquence. 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é dans un autre ? Le nouveau rappel d'inactivité sera planifié pour s'exécuter dès que possible, à partir du cadre suivant (plutôt que du cadre actuel).

Inactif activé !

requestIdleCallback est un excellent moyen de vous assurer que vous pouvez exécuter votre code, mais sans gêner l'utilisateur. Il est simple à utiliser et très flexible. Nous en sommes encore aux premiers stades de développement, et la spécification n'est pas encore finalisée. Tous vos commentaires sont donc les bienvenus.

Essayez-la dans Chrome Canary, testez-la pour vos projets et dites-nous ce que vous en pensez !