Limitation importante des minuteurs JS enchaînés à partir de Chrome 88

Jake Archibald
Jake Archibald

Chrome 88 (janvier 2021) limite fortement les minuteurs JavaScript en chaîne pour les pages masquées dans certaines conditions. Cela réduira l'utilisation du processeur, ainsi que l'utilisation de la batterie. Dans certains cas extrêmes, cela peut modifier le comportement, mais les minuteurs sont souvent utilisés lorsqu'une autre API serait plus efficace et plus fiable.

OK, j'ai beaucoup de jargon et un peu d'ambiguïté. C'est parti...

Terminologie

Pages masquées

En général, l'état masquée signifie qu'un autre onglet est actif ou que la fenêtre a été réduite. Toutefois, les navigateurs peuvent considérer une page masquée lorsque son contenu est totalement invisible. Certains navigateurs sont plus poussés que d'autres, mais vous pouvez toujours utiliser l'API de visibilité des pages pour savoir quand le navigateur pense que la visibilité a changé.

Minuteurs JavaScript

Par minuteurs JavaScript, j'entends setTimeout et setInterval, qui vous permettent de planifier un rappel ultérieurement. Les minuteurs sont utiles et ne disparaissent pas, mais ils sont parfois utilisés pour interroger l'état lorsqu'un événement serait plus efficace et plus précis.

Minuteurs en chaîne

Si vous appelez setTimeout dans la même tâche qu'un rappel setTimeout, le deuxième appel est "enchaîné". Avec setInterval, chaque itération fait partie de la chaîne. Cela peut être plus facile à comprendre avec du code:

let chainCount = 0;

setInterval(() => {
  chainCount++;
  console.log(`This is number ${chainCount} in the chain`);
}, 500);

Et :

let chainCount = 0;

function setTimeoutChain() {
  setTimeout(() => {
    chainCount++;
    console.log(`This is number ${chainCount} in the chain`);
    setTimeoutChain();
  }, 500);
}

Fonctionnement de la limitation

La limitation s'effectue par étapes:

Limitation minimale

Cela se produit pour les minuteurs programmés lorsque l'une des conditions suivantes est remplie:

  • La page est visible.
  • La page a fait du bruit au cours des 30 dernières secondes. Cela peut provenir de n'importe quelle API de création de sons, mais une piste audio silencieuse n'est pas prise en compte.

Le minuteur n'est pas limité, sauf si le délai avant expiration demandé est inférieur à 4 ms et que le nombre de chaînes est supérieur ou égal à 5, auquel cas le délai avant expiration est défini sur 4 ms. Ce n'est pas nouveau : les navigateurs le font depuis de nombreuses années.

Limitations

Cela se produit pour les minuteurs programmés lorsqu'une limitation minimale ne s'applique pas et que l'une des conditions suivantes est remplie:

  • Le nombre de chaînes est inférieur à 5.
  • La page est masquée depuis moins de cinq minutes.
  • WebRTC est en cours d'utilisation. Plus précisément, il existe un RTCPeerConnection avec un RTCDataChannel "ouvert" ou un MediaStreamTrack "en direct".

Le navigateur vérifie les minuteurs de ce groupe une fois par seconde. Étant donné qu'ils ne sont vérifiés qu'une fois par seconde, les minuteurs avec un délai d'inactivité similaire sont regroupés, ce qui consolide le temps nécessaire à l'onglet pour exécuter le code. Ce n'est pas nouveau non plus, les navigateurs le font depuis des années.

Limitation intensive

OK, voici la nouvelle interface de Chrome 88. Une limitation intensive se produit pour les minuteurs qui sont planifiés lorsqu'aucune des conditions de limitation minimale ou de limitation ne s'applique, et que toutes les conditions suivantes sont remplies:

  • La page est masquée depuis plus de cinq minutes.
  • Le nombre de chaînes est supérieur ou égal à 5.
  • La page est restée silencieuse pendant au moins 30 secondes.
  • WebRTC n'est pas utilisé.

Dans ce cas, le navigateur vérifie les minuteurs de ce groupe une fois par minute. Comme précédemment, cela signifie que les minuteurs seront regroupés pour ces vérifications minute par minute.

Solutions

Il existe généralement une meilleure alternative au minuteur, ou les minuteurs peuvent être combinés avec autre chose pour être plus respectueux des processeurs et de l'autonomie de la batterie.

Sondage d'État

Il s'agit de l'utilisation la plus courante (mauvaise) des minuteurs. Ils permettent de vérifier ou d'interroger en permanence pour voir si quelque chose a changé. Dans la plupart des cas, il existe un équivalent push, qui vous informe de la modification quand elle se produit. Vous n'avez donc pas besoin de continuer à vérifier. Vérifiez s'il existe un événement qui aboutit au même résultat.

Voici quelques exemples :

Il existe également des déclencheurs de notification si vous souhaitez afficher une notification à un moment précis.

Animation

L'animation étant un élément visuel, elle ne doit pas utiliser le temps CPU lorsque la page est masquée.

requestAnimationFrame est beaucoup plus efficace pour planifier des tâches d'animation que les minuteurs JavaScript. Elle se synchronise avec la fréquence d'actualisation de l'appareil, ce qui vous garantit de n'obtenir qu'un seul rappel par frame affichable et d'obtenir le temps maximal pour construire ce frame. En outre, requestAnimationFrame attend que la page soit visible et n'utilise aucun processeur lorsqu'elle est masquée.

Si vous pouvez déclarer toute votre animation à l'avance, envisagez d'utiliser des animations CSS ou l'API d'animations Web. Elles présentent les mêmes avantages que requestAnimationFrame, mais le navigateur peut effectuer des optimisations supplémentaires, telles que la composition automatique, et sont généralement plus faciles à utiliser.

Si votre animation a une fréquence d'images faible (comme un curseur clignotant), les minuteurs restent la meilleure option pour le moment, mais vous pouvez les combiner avec requestAnimationFrame pour profiter du meilleur des deux mondes:

function animationInterval(ms, signal, callback) {
  const start = document.timeline.currentTime;

  function frame(time) {
    if (signal.aborted) return;
    callback(time);
    scheduleFrame(time);
  }

  function scheduleFrame(time) {
    const elapsed = time - start;
    const roundedElapsed = Math.round(elapsed / ms) * ms;
    const targetNext = start + roundedElapsed + ms;
    const delay = targetNext - performance.now();
    setTimeout(() => requestAnimationFrame(frame), delay);
  }

  scheduleFrame(start);
}

Utilisation :

const controller = new AbortController();

// Create an animation callback every second:
animationInterval(1000, controller.signal, time => {
  console.log('tick!', time);
});

// And stop it:
controller.abort();

Test

Cette modification s'appliquera à tous les utilisateurs de Chrome dans Chrome 88 (janvier 2021). Elle est actuellement activée pour 50% des utilisateurs de la version bêta, en développement et Canary de Chrome. Si vous souhaitez le tester, utilisez cet indicateur de ligne de commande lors du lancement de la version bêta, de la version en développement ou de la version Canary de Chrome:

--enable-features="IntensiveWakeUpThrottling:grace_period_seconds/10,OptOutZeroTimeoutTimersFromThrottling,AllowAggressiveThrottlingWithWebSocket"

L'argument grace_period_seconds/10 entraîne un ralentissement intense après 10 secondes de masquage de la page, au lieu des 5 minutes complètes, ce qui permet de mieux voir l'impact de la limitation.

L'avenir

Étant donné que les minuteurs sont une source d'utilisation excessive du processeur, nous allons continuer à chercher des moyens de les limiter sans altérer le contenu Web, et des API que nous pouvons ajouter/modifier pour répondre à des cas d'utilisation. Personnellement, je souhaite éliminer la nécessité d'utiliser animationInterval au profit de rappels d'animation basse fréquence efficaces. Si vous avez des questions, contactez-moi sur Twitter.

Photo d'en-tête par Heather Zabriskie sur Unsplash.