De nombreux sites et applications ont de nombreux scripts à exécuter. Votre code JavaScript doit souvent être exécuté dès que possible, mais vous ne voulez pas qu'il gêne 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.

La bonne nouvelle est qu'une API 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. Elle est disponible à partir de Chrome 47. Vous pouvez donc l'essayer dès aujourd'hui avec 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 la fonctionnalité, juste pour être absolument sûr que l'utilisateur n'interagit pas. En revanche, le navigateur sait exactement combien de temps est disponible à la fin du frame et si l'utilisateur interagit. Grâce à requestIdleCallback
, nous disposons donc d'une API qui nous permet d'utiliser tout temps libre de la manière la plus efficace 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 ajuster 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 il 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 serez pas pénalisé par le shiming 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 à faire:
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é de timeout. 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'objetdeadline
sera définie sur "true".
Si vous constatez que didTimeout
est défini sur "true", vous voudrez probablement simplement exécuter la tâche et en finir:
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 (la tâche peut entraîner l'arrêt de réponse ou des à-coups de votre application), 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 envoyer des données d'analyse à l'aide de requestIdleCallback
. Dans ce cas, vous voudrez probablement suivre un événement tel que l'appui 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 avant expiration pour s'assurer qu'il n'entre pas en conflit avec les interactions et ne provoque pas de saccades.
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 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 du frame, il sera planifié pour s'exécuter après l'enregistrement du frame actuel, ce qui signifie que les modifications de style auront été appliquées et, surtout, que la mise en page aura é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 de ne pas déclencher de modifications du DOM dans le rappel d'inactivité est que l'impact temporel de la modification du DOM est imprévisible. Nous pourrions donc facilement dépasser le délai fourni 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 vous devez appliquer les correctifs DOM dans le prochain rappel requestAnimationFrame
, 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. Toutefois, il existe un shim si vous souhaitez rediriger de manière transparente vers
setTimeout
. Cette API existe, car 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 quesetTimeout
,setInterval
ousetImmediate
peuvent être utilisées pour planifier des tâches, mais elles ne sont pas chronométrées pour éviter les interactions utilisateur comme le faitrequestIdleCallback
. - Que se passe-t-il si je dépasse la date limite ?
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. Toutefois, le navigateur vous donne un délai pour essayer de garantir une expérience fluide à vos utilisateurs. Par conséquent, sauf si vous avez une très bonne raison, vous devez toujours respecter ce délai. - Existe-t-il une valeur maximale que
timeRemaining()
peut renvoyer ? Oui, il est actuellement de 50 ms. Pour maintenir une application responsive, 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 l'un après l'autre (si le navigateur détermine qu'il 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 effectuer de modifications 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. - Obtiendrai-je toujours un
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 rappelsrequestAnimationFrame
. 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 n'est bloqué par le 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 !