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-Auslastung reduziert, was sich auch auf die Akkunutzung auswirkt. In einigen Grenzfällen ändert sich dadurch das Verhalten. Timer werden jedoch häufig dort verwendet, wo eine andere API effizienter und zuverlässiger wäre.

Okay, das war ziemlich jargonlastig und ein bisschen unklar. Sehen wir uns das genauer an…

Terminologie

Ausgeblendete Seiten

Im Allgemeinen bedeutet ausgeblendet, dass ein anderer Tab aktiv ist oder das Fenster minimiert wurde. In Browsern wird eine Seite jedoch als ausgeblendet betrachtet, wenn ihr Inhalt nicht sichtbar ist. Einige Browser gehen hier weiter als andere. Sie können jedoch jederzeit die Page Visibility API verwenden, um zu verfolgen, wann die Sichtbarkeit laut Browser geändert wurde.

JavaScript-Timer

Mit JavaScript-Timern meine ich setTimeout und setInterval, mit denen Sie einen Callback für die Zukunft planen können. Timer sind nützlich und werden auch in Zukunft verwendet. Manchmal werden sie jedoch verwendet, um den Status abzufragen, obwohl ein Ereignis effizienter und genauer wäre.

Verkettete Timer

Wenn Sie setTimeout in derselben Aufgabe als setTimeout-Rückruf aufrufen, wird die zweite Aufrufe „verkettet“. Bei setInterval ist jede Iteration Teil der Kette. Mit Code ist das vielleicht leichter verständlich:

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

So funktioniert die Drosselung

Die Drosselung erfolgt in mehreren Phasen:

Minimale Drosselung

Das passiert bei Timern, die geplant sind, wenn eine der folgenden Bedingungen erfüllt ist:

  • Die Seite ist sichtbar.
  • Auf der Seite wurden in den letzten 30 Sekunden Geräusche erzeugt. Das kann von einer beliebigen API für die Erstellung von Tönen stammen, ein stummgeschalteter Audiotrack zählt jedoch nicht.

Der Timer wird nur dann gedrosselt, wenn das angeforderte Zeitlimit weniger als 4 ms beträgt und die Anzahl der Ketten mindestens 5 beträgt. In diesem Fall wird das Zeitlimit auf 4 ms festgelegt. Das ist nicht neu. Browser tun dies schon seit vielen Jahren.

Drosselung

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

  • Die Kettenanzahl ist kleiner als 5.
  • Die Seite ist seit weniger als 5 Minuten ausgeblendet.
  • WebRTC wird verwendet. Konkret gibt es eine RTCPeerConnection mit einer RTCDataChannel vom Typ „offen“ oder einer MediaStreamTrack vom Typ „live“.

Der Browser prüft die Timer in dieser Gruppe einmal pro Sekunde. Da sie nur einmal pro Sekunde geprüft werden, werden Timer mit einer ähnlichen Zeitüberschreitung zusammengefasst, sodass der Tab weniger Zeit für die Ausführung von Code benötigt. Das ist auch nichts Neues. Browser tun dies schon seit Jahren in gewissem Maße.

Intensive Drosselung

Okay, hier ist das Neue in Chrome 88. Eine intensive Drosselung erfolgt bei Timern, die geplant werden, wenn keine der Bedingungen für die minimale Drosselung oder Drosselung zutrifft und alle der folgenden Bedingungen erfüllt sind:

  • Die Seite ist seit mehr als 5 Minuten ausgeblendet.
  • Die Kettenanzahl ist mindestens 5.
  • Auf der Seite ist seit mindestens 30 Sekunden keine Aktivität zu sehen.
  • WebRTC wird nicht verwendet.

In diesem Fall prüft der Browser die Timer in dieser Gruppe einmal pro Minute. Ähnlich wie bisher werden Timer bei diesen stündlichen Prüfungen zusammengefasst.

Problemumgehungen

Es gibt in der Regel eine bessere Alternative zu einem Timer oder Timer können mit etwas anderem kombiniert werden, um die CPU und die Akkulaufzeit zu schonen.

Zustandsabfragen

Dies ist die häufigste (Miss-)Nutzung von Timern, bei der sie kontinuierlich verwendet werden, um zu prüfen oder abzufragen, 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 nachsehen müssen. Prüfen Sie, ob es ein Ereignis gibt, das dasselbe bewirkt.

Beispiele:

Es gibt auch Benachrichtigungsauslöser, wenn Sie eine Benachrichtigung zu einer bestimmten Zeit anzeigen lassen möchten.

Animation

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

requestAnimationFrameeignet sich viel besser zum Planen von Animationsaufgaben als JavaScript-Timer. Er wird mit der Bildwiederholrate des Geräts synchronisiert, sodass Sie nur einen Rückruf pro darstellbaren Frame erhalten und die maximale Zeit zum Erstellen dieses Frames haben. Außerdem wartet requestAnimationFrame, bis die Seite sichtbar ist, sodass keine CPU verwendet wird, wenn die Seite ausgeblendet ist.

Wenn Sie die gesamte Animation vorab deklarieren können, sollten Sie CSS-Animationen oder die Web Animations API verwenden. Sie bieten die gleichen Vorteile wie requestAnimationFrame, aber der Browser kann zusätzliche Optimierungen wie automatisches Compositing ausführen. Außerdem sind sie in der Regel einfacher zu verwenden.

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

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

Test

Diese Änderung wird in Chrome 88 (Januar 2021) für alle Chrome-Nutzer aktiviert. Derzeit ist sie für 50% der Chrome Beta-, Dev- und Canary-Nutzer aktiviert. Wenn Sie es testen möchten, verwenden Sie beim Starten von Chrome Beta, Dev oder Canary dieses Befehlszeilen-Flag:

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

Mit dem Argument grace_period_seconds/10 wird die intensive Drosselung bereits nach 10 Sekunden nach dem Ausblenden der Seite aktiviert, anstatt erst nach 5 Minuten. So lässt sich die Auswirkung der Drosselung leichter erkennen.

Die Zukunft

Da Timer eine Quelle für eine übermäßige CPU-Auslastung sind, suchen wir nach Möglichkeiten, sie zu drosseln, ohne Webinhalte zu beeinträchtigen, und nach APIs, die wir hinzufügen oder ändern können, um Anwendungsfälle zu erfüllen. Ich persönlich würde animationInterval durch effiziente Animations-Callbacks mit niedriger Frequenz ersetzen. Bei Fragen kannst du dich jederzeit auf Twitter an mich wenden.

Titelbild von Heather Zabriskie auf Unsplash.