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

Jake Archibald
Jake Archibald

Chrome 88 (janvier 2021) limitera fortement les minuteurs JavaScript en série pour les pages masquées dans des conditions particulières. Cela réduit l'utilisation du processeur, ce qui réduit également l'utilisation de la batterie. Dans certains cas particuliers, cela peut modifier le comportement, mais les minuteurs sont souvent utilisés là où une API différente serait plus efficace et plus fiable.

OK, c'était assez jargonneux et un peu ambigu. Voyons cela ensemble.

Terminologie

Pages masquées

En général, masqué signifie qu'un autre onglet est actif ou que la fenêtre a été réduite, mais les navigateurs peuvent considérer qu'une page est masquée chaque fois que son contenu est totalement invisible. Certains navigateurs vont plus loin que d'autres, mais vous pouvez toujours utiliser l'API Page Visibility pour suivre quand le navigateur pense que la visibilité a changé.

Retardateurs JavaScript

Par délais JavaScript, j'entends setTimeout et setInterval, qui vous permettent de planifier un rappel à un moment donné. Les minuteurs sont utiles et ne vont pas disparaître, mais ils sont parfois utilisés pour interroger l'état lorsqu'un événement serait plus efficace et plus précis.

Minuteurs en série

Si vous appelez setTimeout dans la même tâche qu'un rappel setTimeout, la deuxième invocation est "enchaînée". 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 du débit limité

Le débit est limité 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 émis des bruits au cours des 30 dernières secondes. Il peut s'agir de n'importe quelle API de création de son, mais une piste audio silencieuse ne compte pas.

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 égal ou supérieur à cinq, auquel cas le délai avant expiration est défini sur 4 ms. Il ne s'agit pas d'une nouveauté. Les navigateurs font cela depuis de nombreuses années.

Limitations

Cela se produit pour les minuteurs programmés lorsque le bloquant minimal ne s'applique pas et que l'une des conditions suivantes est remplie:

  • Le nombre de chaînes est inférieur à cinq.
  • La page a été masquée pendant moins de cinq minutes.
  • WebRTC est utilisé. 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 avant expiration similaire seront regroupés, ce qui consolidera le temps dont l'onglet a besoin pour exécuter le code. Il ne s'agit pas non plus d'une nouveauté. Les navigateurs font cela dans une certaine mesure depuis des années.

Limitation intensive

OK, voici la nouveauté de Chrome 88. Le forçage intensif s'applique aux minuteurs 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 égal ou supérieur à cinq.
  • La page est silencieuse depuis 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 auparavant, cela signifie que les minuteurs seront regroupés dans ces vérifications par minute.

Solutions

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

Interrogation de l'état

Il s'agit de l'utilisation (dé)mauvaise la plus courante des minuteurs, qui sont utilisés pour effectuer des vérifications ou des requêtes continues afin de voir si quelque chose a changé. Dans la plupart des cas, il existe un équivalent de push, où l'élément vous informe du changement lorsqu'il se produit, de sorte que vous n'ayez pas à vérifier en permanence. Vérifiez si un événement permet d'obtenir le même résultat.

Voici quelques exemples :

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

Animation

L'animation est un élément visuel. Elle ne doit donc pas utiliser de temps de processeur lorsque la page est masquée.

requestAnimationFrame est beaucoup plus efficace pour planifier le travail d'animation que les minuteurs JavaScript. Il se synchronise avec la fréquence d'actualisation de l'appareil, ce qui garantit que vous ne recevez qu'un seul rappel par frame visible et que vous disposez du temps maximal pour construire ce frame. De plus, requestAnimationFrame attend que la page soit visible. Il n'utilise donc aucun processeur lorsque la page est masquée.

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

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

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();

Tests

Cette modification sera activée pour tous les utilisateurs de Chrome dans Chrome 88 (janvier 2021). Elle est actuellement activée pour 50% des utilisateurs de Chrome Bêta, en développement et Canary. Si vous souhaitez le tester, utilisez l'indicateur de ligne de commande suivant lorsque vous lancez Chrome Bêta, Dev ou Canary:

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

L'argument grace_period_seconds/10 déclenche une limitation intense au bout de 10 secondes après la masquage de la page, au lieu de cinq minutes complètes, ce qui permet de voir plus facilement 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 endommager le contenu Web, ainsi que des API que nous pouvons ajouter/modifier pour répondre aux cas d'utilisation. Personnellement, j'aimerais éliminer le besoin de animationInterval au profit de rappels d'animation à faible fréquence efficaces. Si vous avez des questions, n'hésitez pas à me contacter sur Twitter.

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