Starke Drosselung verketteter JS-Timer ab Chrome 88

Jake Archibald
Jake Archibald

In Chrome 88 (Januar 2021) werden verkettete JavaScript-Timer für ausgeblendete Seiten unter bestimmten Bedingungen stark gedrosselt. Dadurch wird die CPU-Nutzung und damit auch die Akkunutzung reduziert. Es gibt einige Grenzfälle, in denen sich dies auf das Verhalten auswirkt, aber Timer werden häufig verwendet, wenn eine andere API effizienter und zuverlässiger wäre.

Okay, das war ziemlich viel Fachjargon und ein bisschen mehrdeutig. Los gehts...

Terminologie

Ausgeblendete Seiten

Im Allgemeinen bedeutet verborgen, dass ein anderer Tab aktiv ist oder das Fenster minimiert wurde. Browser betrachten eine Seite jedoch möglicherweise als ausgeblendet, wenn ihr Inhalt völlig nicht sichtbar ist. Einige Browser gehen hier noch weiter als andere. Sie können aber jederzeit mit der Page visibility API feststellen, wann der Browser feststellt, dass sich die Sichtbarkeit geändert hat.

JavaScript-Timer

Mit JavaScript-Timern meine ich setTimeout und setInterval. Mit diesen Timern können Sie einen Callback in der Zukunft planen. Timer sind nützlich und laufen nicht ab. Manchmal werden sie jedoch verwendet, um den Status abzufragen, wenn ein Ereignis effizienter und genauer wäre.

Verkettete Timer

Wenn Sie setTimeout in derselben Aufgabe wie ein setTimeout-Callback aufrufen, wird der zweite Aufruf „verkettet“. Bei setInterval ist jede Iteration Teil der Kette. Dies könnte mit Code einfacher zu verstehen sein:

let chainCount = 0;

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

Und:

let chainCount = 0;

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

Funktionsweise der Drosselung

Die Drosselung erfolgt stufenweise:

Minimale Drosselung

Das ist bei geplanten Timern der Fall, wenn eine der folgenden Bedingungen zutrifft:

  • Die Seite ist sichtbar.
  • Auf der Seite wurden in den letzten 30 Sekunden Geräusche ausgegeben. Sie kann von einer beliebigen Toner-API stammen, lautlose Audiospur zählt aber nicht.

Der Timer wird nicht gedrosselt, es sei denn, das angeforderte Zeitlimit liegt unter 4 ms und die Kettenanzahl ist mindestens 5. In diesem Fall wird das Zeitlimit auf 4 ms festgelegt. Dies ist nicht neu, das tun Browser schon seit vielen Jahren.

Drosselung

Das passiert bei Timern, die geplant sind, wenn keine minimale Drosselung zutrifft und eine der folgenden Bedingungen zutrifft:

  • Die Anzahl der Ketten ist kleiner als 5.
  • Die Seite ist seit weniger als 5 Minuten ausgeblendet.
  • WebRTC wird verwendet. Insbesondere gibt es ein RTCPeerConnection mit einem „offenen“ RTCDataChannel oder einem „live“-MediaStreamTrack.

Der Browser prüft die Timer in dieser Gruppe einmal pro Sekunde. Da sie nur einmal pro Sekunde geprüft werden, werden Timer mit einem ähnlichen Zeitlimit zu Batches zusammengefasst, sodass die Zeit konsolidiert wird, die der Tab zum Ausführen von Code benötigt. Das ist auch nicht neu. Browser machen das schon seit Jahren.

Intensives Drosseln

Ok, hier ist die neue Version in Chrome 88. Die intensive Drosselung erfolgt bei Timern, die geplant sind, wenn keine der Bedingungen für minimale Drosselung oder Drosselung zutrifft und alle der folgenden Bedingungen erfüllt sind:

  • Die Seite ist seit mehr als 5 Minuten ausgeblendet.
  • Die Kettenanzahl beträgt mindestens 5.
  • Die Seite wurde mindestens 30 Sekunden lang nicht angezeigt.
  • WebRTC wird nicht verwendet.

In diesem Fall prüft der Browser die Timer in dieser Gruppe einmal pro Minute. Ähnlich wie zuvor bedeutet dies, dass die Timer bei diesen minutengenauen Prüfungen in Batches zusammengefasst werden.

Problemumgehungen

Es gibt normalerweise eine bessere Alternative zu einem Timer. Du kannst auch Timer mit etwas anderem kombinieren, um die CPU und die Akkulaufzeit zu optimieren.

Staatliche Umfragen

Dies ist die häufigste (falsche) Verwendung von Timern, mit denen kontinuierlich überprüft oder abgefragt wird, um festzustellen, ob sich etwas geändert hat. In den meisten Fällen gibt es ein push-Äquivalent, bei dem Sie über die Änderung informiert werden, sobald sie eintritt, sodass Sie nicht ständig überprüfen müssen. Prüfen Sie, ob es ein Ereignis gibt, das die gleiche Leistung erzielt.

Beispiele:

Es gibt auch Benachrichtigungsauslöser, wenn eine Benachrichtigung zu einem bestimmten Zeitpunkt angezeigt werden soll.

Animation

Animationen sind visueller Natur und sollten keine CPU-Zeit verbrauchen, wenn die Seite ausgeblendet ist.

Mit requestAnimationFrame lassen sich Animationen viel besser planen als JavaScript-Timer. Er wird mit der Aktualisierungsrate des Geräts synchronisiert, sodass Sie nur einen Callback pro anzeigbarem Frame erhalten und die maximale Zeit zum Erstellen dieses Frames bleibt. Außerdem wartet requestAnimationFrame, bis die Seite sichtbar ist, und verbraucht also keine CPU, wenn die Seite ausgeblendet ist.

Wenn Sie die gesamte Animation im Voraus deklarieren können, sollten Sie CSS-Animationen oder die Web Animations API verwenden. Diese haben dieselben Vorteile wie requestAnimationFrame, der Browser kann jedoch zusätzliche Optimierungen wie automatische Zusammensetzung vornehmen und sind im Allgemeinen einfacher zu verwenden.

Wenn Ihre Animation eine niedrige Framerate hat (z. B. ein blinkender Cursor), sind Timer im Moment immer noch die beste Option. Sie können sie aber mit requestAnimationFrame kombinieren, um das Beste aus beiden Welten zu machen:

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

Verwendung:

const controller = new AbortController();

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

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

Testen

Diese Änderung wird für alle Chrome-Nutzer in Chrome 88 (Januar 2021) aktiviert. Sie ist derzeit für 50% der Chrome Beta-, Entwickler- und Canary-Nutzer aktiviert. Wenn Sie ihn testen möchten, verwenden Sie dieses Befehlszeilen-Flag, wenn Sie Chrome Beta, Dev oder Canary starten:

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

Das Argument grace_period_seconds/10 führt dazu, dass die intensive Drosselung nach 10 Sekunden, nachdem die Seite ausgeblendet wurde, statt nach den vollen 5 Minuten aktiviert wird. Dadurch lassen sich die Auswirkungen der Drosselung leichter erkennen.

Die Zukunft

Da Timer eine Ursache für eine übermäßige CPU-Auslastung sind, werden wir weiterhin nach Möglichkeiten suchen, sie zu drosseln, ohne dass Webinhalte beschädigt werden, und APIs, die wir für Anwendungsfälle hinzufügen oder ändern können. Ich persönlich möchte auf animationInterval verzichten und stattdessen effiziente Callbacks für Animationen mit niedriger Häufigkeit nutzen. Wenn Sie Fragen haben, kontaktieren Sie mich einfach auf Twitter.

Titelfoto von Heather Zabriskie auf Unsplash.