チェーン化された JS タイマーの大規模なスロットリング(Chrome 88 以降)

Chrome 88(2021 年 1 月)では、特定の条件下で、非表示のページのチェーンされた JavaScript タイマーが大幅にスロットリングされます。これにより CPU 使用量が減り、バッテリー使用量も減ります。動作が変化するエッジケースもありますが、タイマーは、別の API の方が効率的で信頼性が高い場合によく使用されます。

専門用語が多く、ややあいまいな説明でした。詳しく見ていきましょう。

用語

非表示のページ

通常、非表示とは、別のタブがアクティブになっているか、ウィンドウが最小化されていることを意味しますが、ブラウザでは、コンテンツが完全に表示されていない場合は、ページが非表示と見なされることがあります。ブラウザによっては、この点において他よりも進んでいるものもありますが、Page Visibility API を使用して、ブラウザが可視性が変更されたと判断したタイミングを常に追跡できます。

JavaScript タイマー

JavaScript タイマーとは、setTimeoutsetInterval を指します。これにより、将来のコールバックをスケジュールできます。タイマーは便利で、廃止されることはありませんが、イベントの方が効率的で正確な場合に、状態をポーリングするために使用されることがあります。

連鎖タイマー

setTimeout コールバックと同じタスクで setTimeout を呼び出すと、2 回目の呼び出しは「連鎖」されます。setInterval を使用すると、各反復処理はチェーンの一部になります。コードで説明するとわかりやすいでしょう。

let chainCount = 0;

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

および:

let chainCount = 0;

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

スロットリングの仕組み

スロットリングは段階的に行われます。

最小限のスロットリング

これは、次のいずれかが当てはまる場合に、スケジュール設定されたタイマーで発生します。

  • ページが表示されている
  • 過去 30 秒間にページからノイズが聞こえた。これは、任意のサウンド作成 API から取得できますが、サイレント音声トラックはカウントされません。

タイマーは、リクエストされたタイムアウトが 4 ミリ秒未満で、チェーン数が 5 以上の場合を除き、スロットリングされません。この場合、タイムアウトは 4 ミリ秒に設定されます。これは新しい機能ではなく、ブラウザでは長年行われてきました。

スロットル処理

これは、最小スロットリングが適用されず、次のいずれかに該当する場合にスケジュールされたタイマーで発生します。

  • チェーン数が 5 未満です。
  • ページが非表示になってから 5 分未満である。
  • WebRTC が使用されています。具体的には、RTCPeerConnection に「open」RTCDataChannel または「live」MediaStreamTrack があります。

ブラウザは、このグループ内のタイマーを 1 に 1 回チェックします。タイマーは 1 秒に 1 回しかチェックされないため、タイムアウトが同じタイマーはまとめて処理され、タブがコードを実行するために必要な時間が統合されます。これは新しいものではありません。ブラウザは長年にわたって、ある程度この処理を行っています。

集中的なスロットリング

Chrome 88 の新機能は次のとおりです。集中的なスロットリングは、最小スロットリング条件またはスロットリング条件のいずれも適用されず、以下の条件がすべて満たされている場合に、スケジュールされたタイマーに対して行われます。

  • ページが 5 分以上非表示になっている。
  • チェーン数が 5 以上。
  • ページが 30 秒以上静音状態である。
  • WebRTC は使用されていません。

この場合、ブラウザは 1 に 1 回、このグループ内のタイマーをチェックします。以前と同様に、タイマーは 1 分ごとのチェックでまとめて処理されます。

回避策

通常、タイマーに代わる優れた方法があります。また、タイマーを他のものと組み合わせて、CPU とバッテリー駆動時間を節約することもできます。

状態ポーリング

これはタイマーの最も一般的な(誤った)使用方法です。タイマーを使用して、何かが変更されていないか継続的にチェックまたはポーリングします。ほとんどの場合、同等のpush があり、変更が発生したときに通知されるため、常に確認する必要はありません。同じことを実現するイベントがあるかどうかを確認します。

例:

特定の時間に通知を表示する場合は、通知トリガーも使用できます。

アニメーション

アニメーションは視覚的な要素であるため、ページが非表示になっているときに CPU 時間を使用すべきではありません。

requestAnimationFrame は、JavaScript タイマーよりもアニメーション処理のスケジュール設定に優れています。デバイスのリフレッシュ レートと同期されるため、表示可能なフレームごとに 1 回だけコールバックが実行され、そのフレームの作成に最大限の時間を確保できます。また、requestAnimationFrame はページが表示されるのを待機するため、ページが非表示になっているときは CPU を使用しません。

アニメーション全体を事前に宣言できる場合は、CSS アニメーションまたは ウェブ アニメーション API の使用を検討してください。これらには requestAnimationFrame と同じ利点がありますが、ブラウザは自動合成などの追加の最適化を実行でき、一般的に使いやすくなっています。

アニメーションのフレームレートが低い場合(点滅するカーソルなど)、現時点ではタイマーが最適なオプションですが、タイマーを requestAnimationFrame と組み合わせることで、両方のメリットを活用できます。

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

使用方法:

const controller = new AbortController();

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

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

テスト

この変更は、Chrome 88(2021 年 1 月)ですべての Chrome ユーザーに対して有効になります。現在、Chrome ベータ版、Dev、Canary の各チャンネルを使用するユーザーの 50% に対して有効になっています。テストする場合は、Chrome ベータ版、Dev、Canary を起動するときに、次のコマンドライン フラグを使用します。

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

grace_period_seconds/10 引数を使用すると、ページが非表示になってから 5 分間ではなく 10 秒後に強力なスロットリングが開始されるため、スロットリングの影響を簡単に確認できます。

今後について

タイマーは CPU 使用量の増加の原因となるため、ウェブ コンテンツを損なうことなくタイマーをスロットリングする方法と、ユースケースに合わせて追加または変更できる API を継続的に検討していきます。個人的には、animationInterval の必要性を排除し、低頻度のアニメーション コールバックを効率的に使用することをおすすめします。ご不明な点がございましたら、Twitter でお問い合わせください。

ヘッダー写真は Heather Zabriskie による Unsplash のものです。